Build a Credit Alert System in Java
Build a Spring Boot app that emails users when electricity credits run low.
Introduction
30 Second Summary
Prepaid electricity works like a prepaid phone plan. When credits run out, the power cuts off without warning.
In this project, you will build a Spring Boot web application that monitors household electricity credits. The system automatically sends email alerts through Gmail when balances run low, giving households time to top up before they lose power.
What You'll Build
Picture adding a household from your phone, watching their credit balance drain in real time, and finding a warning email in your inbox just before the power would cut off.
By the end of this project, you'll have:
- A responsive admin dashboard where you register households, assign electricity credits, and monitor balances from your phone or computer.
- Automatic credit consumption that drains balances every 30 seconds so you can watch simulated electricity usage happen live.
- Email notifications through Gmail that fire automatically when credits drop below 20%, warning households to top up.
- Secret Mission: an optional challenge to push your skills further.
Are there any prerequisites?
This project assumes basic Java knowledge, including classes, methods, and running programs.
You will also need JDK 21, Maven 3.9.15, IntelliJ IDEA, and a Gmail account with 2-Step Verification enabled.
Before We Start
Before we start building, take a moment to think about what you're creating and why it matters.
You're about to build a system that watches household electricity credit balances and fires off email alerts before anyone loses power.
Set Up the Spring Boot Project
You need a working project skeleton before you can build any features. Spring Boot gives you a pre-configured foundation with all the libraries your app needs baked in.
Spring Initializr is a web tool that generates a ready-to-run Spring Boot project in seconds. In this step, you'll use it to create the project, configure your database and email settings, then verify everything starts up.
In this step, get ready to:
- Generate a Spring Boot project with the right dependencies.
- Configure the H2 database, Thymeleaf, and email settings.
- Run the application and verify it starts on localhost:8080.
Generate the project with Spring Initializr
Spring Initializr is a web-based project generator hosted at start.spring.io. You'll pick your build tool, Java version, and dependencies, then download a ready-to-open ZIP file.
- Open your browser and navigate to start.spring.io.
- Set **Project** to Maven.
- Set Language to Java.
- Set Spring Boot to 4.1.0.
- Under Project Metadata, set Artifact to electricity-alerts.
- Set Name to electricity-alerts.
- Set Description to Prepaid Electricity Credit Alert System.
- Set Java to 21.
- Click Add Dependencies and search for each of the following, pressing Enter to select each one:
- Spring Web.
- Spring Data JPA.
- H2 Database.
- Thymeleaf.
- Java Mail Sender.
- Click Generate to download the project ZIP file.
- Extract the ZIP file to your Desktop.
What did Spring Initializr just create?
The ZIP contains a complete Maven project with a pom.xml listing all your dependencies, a main application class, and an empty application.properties file ready for configuration.
The five dependencies you selected pull in everything the project needs: a web server, database access, HTML templating, and email sending.
- Open IntelliJ IDEA by pressing Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type IntelliJ and press Enter to open it.
- Click Open on the welcome screen.
- Navigate to your Desktop, select the electricity-alerts folder, and click OK.
- Wait for IntelliJ to import the project and download Maven dependencies. You will see a progress bar at the bottom of the window.
IntelliJ not recognising the project?
If IntelliJ opens the folder as plain files instead of a Maven project, right-click on pom.xml in the file tree and select Add as Maven Project. IntelliJ will re-import and resolve all dependencies.
Still stuck? Ask for help.
Now open the generated main application class. Spring Initializr created it with the @SpringBootApplication annotation already in place.
- In IntelliJ's Project sidebar, expand src/main/java/com/example/electricityalerts.
- Open ElectricityAlertsApplication.java.
You need to add two things to this file: an import for @EnableScheduling and the annotation itself. This tells Spring Boot to support scheduled tasks, which you will use later to simulate credit consumption and monitor balances.
- Replace the entire contents of ElectricityAlertsApplication.java with the following code:
package com.example.electricityalerts;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
// Main entry point for the application
@SpringBootApplication
@EnableScheduling
public class ElectricityAlertsApplication {
public static void main(String[] args) {
SpringApplication.run(ElectricityAlertsApplication.class, args);
}
}
What does this code do?
@SpringBootApplication marks this as the entry point for the entire Spring Boot app. It bundles together component scanning, auto-configuration, and configuration in one annotation.
@EnableScheduling turns on Spring's task scheduler. Later in the project, you will use @Scheduled methods to automatically drain credits and check for low balances every 30 seconds.
- Save the file by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Configure application properties
The generated application.properties file is nearly empty. You need to tell Spring Boot how to connect to the H2 database, where to find the Thymeleaf templates, and how to reach Gmail's SMTP server for sending emails.
- In the Project sidebar, expand src/main/resources and open application.properties.
- Replace the file contents with the following configuration:
spring.application.name=electricity-alerts
# H2 Database
spring.datasource.url=jdbc:h2:mem:electricitydb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true
spring.jpa.hibernate.ddl-auto=update
# Thymeleaf
spring.thymeleaf.cache=false
# Gmail SMTP
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=your-email@gmail.com
spring.mail.password=your-app-password-here
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.connectiontimeout=5000
spring.mail.properties.mail.smtp.timeout=3000
spring.mail.properties.mail.smtp.writetimeout=5000
What does this configuration do?
- spring.datasource.url=jdbc:h2:mem:electricitydb creates an in-memory database called electricitydb. It lives entirely in RAM, so it resets every time you restart the app. No install needed.
- spring.h2.console.enabled=true exposes a web-based database browser at /h2-console so you can inspect your tables and data directly in the browser.
- spring.thymeleaf.cache=false turns off template caching so changes to your HTML show up immediately when you refresh the browser.
- The Gmail SMTP block configures the connection to Google's mail server on port 587 with TLS encryption. The placeholder values (your-email@gmail.com and your-app-password-here) will be replaced with real credentials in a later step.
- Save the file by pressing Cmd+S (macOS) or Ctrl+S (Windows).
Run the application and verify
Everything is configured. Time to start the application and confirm both the web server and the database are up and running.
- Open IntelliJ's built-in terminal by clicking Terminal at the bottom of the window.
- Start the application by running this command:
mvn spring-boot:run
What does this command do?
mvn spring-boot:run uses Maven to compile your code, download any remaining dependencies, and start the embedded Tomcat web server. The terminal will show startup logs ending with a line that says the application started on port 8080.
You may see a warning about the mail sender not connecting yet. That is expected because the Gmail credentials are still placeholders. The app still starts successfully.
Seeing 'command not found' for mvn?
Make sure Maven 3.9.15 is installed and on your system PATH. Run mvn --version to verify. If you see a version number, Maven is ready. If not, revisit the Maven installation for your operating system.
Help me troubleshoot Maven setup.
Before you check the browser, what do you think you will see at http://localhost:8080? The app has no controllers or pages yet.
- Open your browser and navigate to http://localhost:8080.
You will see a "Whitelabel Error Page" with a 404 status. This is the default Spring Boot page when no controllers are mapped. It confirms the server is running successfully.
- Now navigate to http://localhost:8080/h2-console to open the H2 database console.
- In the JDBC URL field, replace whatever is there with jdbc:h2:mem:electricitydb.
- Leave User Name as sa and Password empty.
- Click Connect.
You should see the H2 console interface load with an empty database. There are no tables yet because you have not created any JPA entities. That happens in the next step.
H2 console showing 'Database not found'?
Make sure the JDBC URL in the console login matches exactly what is in your application.properties: jdbc:h2:mem:electricitydb. The H2 console often defaults to a different URL like jdbc:h2:~/test. You must replace it manually.
Also confirm the application is still running in IntelliJ's terminal. If you stopped it, the in-memory database no longer exists.
Help me connect to the H2 console.
✔️ Awesome, I've got everything!
Great. Double check you have saved both ElectricityAlertsApplication.java and application.properties.
ⓧ I'd like to double check the full code
Here is the complete ElectricityAlertsApplication.java:
package com.example.electricityalerts;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class ElectricityAlertsApplication {
public static void main(String[] args) {
SpringApplication.run(ElectricityAlertsApplication.class, args);
}
}
Here is the complete application.properties:
spring.application.name=electricity-alerts
# H2 Database
spring.datasource.url=jdbc:h2:mem:electricitydb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true
spring.jpa.hibernate.ddl-auto=update
# Thymeleaf
spring.thymeleaf.cache=false
# Gmail SMTP
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=your-email@gmail.com
spring.mail.password=your-app-password-here
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.connectiontimeout=5000
spring.mail.properties.mail.smtp.timeout=3000
spring.mail.properties.mail.smtp.writetimeout=5000
The pom.xml was generated by Spring Initializr. Scroll up to the project generation substep and confirm your five dependencies are listed: spring-boot-starter-web, spring-boot-starter-data-jpa, spring-boot-starter-thymeleaf, spring-boot-starter-mail, and h2.
Your project skeleton is running. The web server is up, the database is accessible, and every dependency is in place. Next up, you'll build the admin dashboard where you can add households and assign electricity credits.
Build the Admin Dashboard
Your Spring Boot project is running and the H2 database is ready to go. Now you need to give the admin a way to register households and assign electricity credits.
Without a dashboard, there is no data for the system to monitor or alert on. In this step, you will create the data model, a web controller, and a responsive admin page using Thymeleaf and Bootstrap.
In this step, get ready to:
- Create the Household entity and database repository.
- Build the admin web page with a form and data table.
- Add test households and verify they appear on the dashboard.
Create the Household entity and repository
A JPA entity is a Java class that maps directly to a database table. Each field becomes a column. You need a Household entity to store the data your admin registers.
- In the Project tool window in IntelliJ, right-click the src/main/java directory.
- Select New, then Java Class.
- Type com.example.electricityalerts.model.Household and press Enter. IntelliJ creates the package folders and the file automatically.
- Replace all the generated content in the file with this code:
package com.example.electricityalerts.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
// Maps this class to a database table
@Entity
public class Household {
// Auto-generated primary key
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String householdName;
private String ownerEmail;
private String phoneNumber;
private int totalCredits;
private int remainingCredits;
// Percentage threshold for low-credit warning
private int warningThreshold;
// Tracks whether an alert has already been sent
private boolean notified;
public Household() {
}
}
What does this code do?
The @Entity annotation tells Spring Data JPA to create a HOUSEHOLD table in the database with a column for each field.
The @Id and @GeneratedValue annotations mark id as an auto-incrementing primary key. The warningThreshold field stores the percentage level at which the system should send an alert. The notified flag prevents duplicate alerts.
- Now add the parameterized constructor below the empty Household() constructor, above the closing } of the class:
// Sets initial credits and default warning threshold
public Household(String householdName, String ownerEmail, String phoneNumber, int totalCredits) {
this.householdName = householdName;
this.ownerEmail = ownerEmail;
this.phoneNumber = phoneNumber;
this.totalCredits = totalCredits;
this.remainingCredits = totalCredits;
this.warningThreshold = 20;
this.notified = false;
}
What does this constructor do?
When a new household is registered, remainingCredits starts equal to totalCredits (a full balance). The warningThreshold defaults to 20, meaning the system will alert when the balance drops below 20% of the total.
- Below the constructor you just added, still above the closing } of the class, add the getters, setters, and a utility method:
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getHouseholdName() { return householdName; }
public void setHouseholdName(String householdName) { this.householdName = householdName; }
public String getOwnerEmail() { return ownerEmail; }
public void setOwnerEmail(String ownerEmail) { this.ownerEmail = ownerEmail; }
public String getPhoneNumber() { return phoneNumber; }
public void setPhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; }
public int getTotalCredits() { return totalCredits; }
public void setTotalCredits(int totalCredits) { this.totalCredits = totalCredits; }
public int getRemainingCredits() { return remainingCredits; }
public void setRemainingCredits(int remainingCredits) { this.remainingCredits = remainingCredits; }
public int getWarningThreshold() { return warningThreshold; }
public void setWarningThreshold(int warningThreshold) { this.warningThreshold = warningThreshold; }
public boolean isNotified() { return notified; }
public void setNotified(boolean notified) { this.notified = notified; }
// Calculates how much credit remains as a percentage
public double getRemainingPercentage() {
if (totalCredits == 0) return 0;
return (double) remainingCredits / totalCredits * 100;
}
Why the getRemainingPercentage method?
The getters and setters let JPA read and write each field. The getRemainingPercentage() method calculates the current balance as a percentage of the total credits. The monitoring service will use this to decide when to trigger an alert.
- Save Household.java.
✔️ Awesome, I've got everything!
Great. Double check you have saved the file.
ⓧ I'd like to double check the full code
Scroll up and confirm your Household.java has all three sections in order:
- The entity class declaration with 8 fields (id through notified) and the empty constructor.
- The parameterized constructor that sets remainingCredits = totalCredits and warningThreshold = 20.
- All getters and setters, plus getRemainingPercentage().
Next, create the repository that gives you database access for this entity.
- Right-click src/main/java again, select New then Java Class.
- Type com.example.electricityalerts.repository.HouseholdRepository and select Interface from the kind dropdown, then press Enter.
- Replace all the generated content with this code:
package com.example.electricityalerts.repository;
import com.example.electricityalerts.model.Household;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface HouseholdRepository extends JpaRepository<Household, Long> {
// Finds all households that have not been notified yet
List<Household> findByNotifiedFalse();
}
What does this code do?
By extending JpaRepository, Spring automatically provides findAll(), save(), deleteById(), and other standard database operations with zero code.
The findByNotifiedFalse() method is a custom query Spring generates from the method name. It returns only households that have not been alerted yet. The monitoring service will use this later.
- Save HouseholdRepository.java.
✔️ Awesome, I've got everything!
Great. Confirm you have saved the file.
ⓧ I'd like to double check the full code
package com.example.electricityalerts.repository;
import com.example.electricityalerts.model.Household;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface HouseholdRepository extends JpaRepository<Household, Long> {
List<Household> findByNotifiedFalse();
}
Before you restart the app, what do you think will happen in the H2 database now that you have an @Entity class?
- Stop the running application by pressing Ctrl+C in the terminal.
- Restart the application by running:
mvn spring-boot:run
- Open http://localhost:8080/h2-console in your browser.
- In the JDBC URL field, enter jdbc:h2:mem:electricitydb (this matches your application.properties). Leave the password blank and click Connect.
You should see a HOUSEHOLD table listed in the left panel. Spring Data JPA automatically created it from your entity class.
Don't see the HOUSEHOLD table?
- Make sure the JDBC URL field says jdbc:h2:mem:electricitydb, not the default jdbc:h2:~/test. The H2 console pre-fills a different URL by default.
- Check that Household.java has the @Entity annotation on the class.
- Confirm your terminal shows the Spring Boot startup banner with no errors.
Why isn't my HOUSEHOLD table showing up in the H2 console?
Build the admin controller and page
You have the data layer. Now you need a Spring MVC controller to handle web requests and a Thymeleaf template to render the admin page.
- Right-click src/main/java, select New then Java Class.
- Type com.example.electricityalerts.controller.AdminController and press Enter.
- Replace the generated content with this code:
package com.example.electricityalerts.controller;
import com.example.electricityalerts.model.Household;
import com.example.electricityalerts.repository.HouseholdRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
public class AdminController {
private final HouseholdRepository householdRepository;
// Spring injects the repository automatically
public AdminController(HouseholdRepository householdRepository) {
this.householdRepository = householdRepository;
}
// Loads all households and passes them to the template
@GetMapping("/admin")
public String adminDashboard(Model model) {
model.addAttribute("households", householdRepository.findAll());
return "admin";
}
}
What does this code do?
The @Controller annotation marks this class as a web request handler. The @GetMapping("/admin") method loads every household from the database and passes them to a Thymeleaf template named admin.
Spring automatically injects the HouseholdRepository through the constructor. This is called dependency injection.
- Now add the POST handler that processes form submissions. Place this code below the adminDashboard method, above the closing } of the class:
// Handles the add-household form submission
@PostMapping("/admin/add")
public String addHousehold(@RequestParam String householdName,
@RequestParam String ownerEmail,
@RequestParam String phoneNumber,
@RequestParam int totalCredits,
RedirectAttributes redirectAttributes) {
Household household = new Household(householdName, ownerEmail, phoneNumber, totalCredits);
householdRepository.save(household);
// Flash message shown after redirect
redirectAttributes.addFlashAttribute("success", "Household added successfully!");
return "redirect:/admin";
}
What does this code do?
The @PostMapping("/admin/add") method receives form data, creates a new Household object, and saves it to the database. After saving, it redirects back to /admin with a success message that appears as a green banner on the page.
- Save AdminController.java.
✔️ Awesome, I've got everything!
Great. Make sure the file is saved.
ⓧ I'd like to double check the full code
Scroll up and confirm your AdminController.java has both methods:
- The adminDashboard GET handler that loads all households.
- The addHousehold POST handler that saves new households and redirects.
Where does the controller find its template?
The return "admin" line tells Spring to look for a file called admin.html inside src/main/resources/templates/. That is the file you will create next.
- In the Project tool window, expand src/main/resources/templates.
- Right-click the templates folder, select New, then File.
- Name it admin.html and press Enter.
- Paste the HTML page structure into the empty file:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Electricity Credit Admin</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css"
rel="stylesheet">
</head>
<body>
<div class="container mt-4">
<h1 class="mb-4">Electricity Credit Dashboard</h1>
<div th:if="${success}" class="alert alert-success" th:text="${success}"></div>
<div class="card mb-4">
<div class="card-header">Add New Household</div>
<div class="card-body">
<form action="/admin/add" method="post">
<div class="row g-3">
<div class="col-md-6">
<label for="householdName" class="form-label">Household Name</label>
<input type="text" class="form-control" id="householdName"
name="householdName" required>
</div>
<div class="col-md-6">
<label for="ownerEmail" class="form-label">Owner Email</label>
<input type="email" class="form-control" id="ownerEmail"
name="ownerEmail" required>
</div>
What does this code do?
The xmlns:th attribute enables Thymeleaf expressions in the HTML. The viewport meta tag makes the page responsive on phones and tablets. Bootstrap 5.3.8 is loaded from a CDN so you get a clean layout without any build tools.
The th:if="${success}" line conditionally shows the green success banner after a household is added. The form posts to /admin/add, which maps to the POST handler you just created.
- Add the remaining form fields and the submit button directly below the code you just pasted:
<div class="col-md-6">
<label for="phoneNumber" class="form-label">Phone Number</label>
<input type="text" class="form-control" id="phoneNumber"
name="phoneNumber" placeholder="+261XXXXXXXXX" required>
</div>
<div class="col-md-6">
<label for="totalCredits" class="form-label">Total Credits</label>
<input type="number" class="form-control" id="totalCredits"
name="totalCredits" min="1" required>
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary w-100">Add Household</button>
</div>
</div>
</form>
</div>
</div>
What does this code do?
The phone field has a +261XXXXXXXXX placeholder showing the Madagascar country code format. The credits field uses type="number" with min="1" so the admin cannot enter zero or negative credits. The submit button spans the full width of the form.
- Below the form card you just closed, add the table that displays all registered households:
<div class="card">
<div class="card-header">All Households</div>
<div class="table-responsive">
<table class="table table-striped mb-0">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Phone</th>
<th>Credits</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr th:each="h : ${households}">
<td th:text="${h.householdName}"></td>
<td th:text="${h.ownerEmail}"></td>
<td th:text="${h.phoneNumber}"></td>
<td>
<span th:text="${h.remainingCredits}"></span> /
<span th:text="${h.totalCredits}"></span>
</td>
What does this code do?
The th:each="h : ${households}" expression loops through every household passed from the controller and renders a table row for each one. The credits column shows remaining out of total (e.g., 50 / 100).
- Add the status badges, the empty-state row, and the closing tags directly below:
<td>
<span th:if="${h.remainingCredits == 0}"
class="badge bg-danger">Depleted</span>
<span th:if="${h.notified and h.remainingCredits > 0}"
class="badge bg-warning text-dark">Low - Notified</span>
<span th:if="${!h.notified and h.remainingCredits > 0}"
class="badge bg-success">OK</span>
</td>
</tr>
<tr th:if="${#lists.isEmpty(households)}">
<td colspan="5" class="text-center text-muted">
No households registered yet.
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>
What do the status badges show?
Each household gets a colour-coded badge. OK (green) means credits are healthy. Low - Notified (yellow) means the system has sent an alert. Depleted (red) means the balance has hit zero.
The empty-state row with #lists.isEmpty(households) displays a message when no households have been registered yet.
- Save admin.html.
✔️ Awesome, I've got everything!
Great. Confirm the file is saved.
ⓧ I'd like to double check the full code
Scroll up and confirm your admin.html has all four sections in order:
- The HTML head with Bootstrap CSS, the container, heading, and success alert.
- The form card with 4 input fields (name, email, phone, credits) and the submit button.
- The table card with 5 column headers (Name, Email, Phone, Credits, Status) and the th:each data row.
- The status badge conditionals, the empty-state row, and the closing tags with Bootstrap JS.
- Stop the running application with Ctrl+C and restart it by running:
mvn spring-boot:run
- Open http://localhost:8080/admin in your browser.
You should see the Electricity Credit Dashboard with the form at the top and an empty table saying No households registered yet. at the bottom.
Seeing a Whitelabel Error Page?
- Check that admin.html is saved inside src/main/resources/templates/, not inside src/main/resources/ directly. Thymeleaf only looks in the templates folder.
- Make sure AdminController.java returns "admin" (without the .html extension) from the adminDashboard method.
- Look at the terminal output for error messages that point to a specific file or line number.
Why am I getting a Whitelabel Error Page when I visit /admin?
Add test households and see them on the dashboard
Your dashboard is live. Time to prove it works by adding real data through the form.
- In the Household Name field, enter Rakoto Family.
- In the Owner Email field, enter your email address.
- In the Phone Number field, enter +261340000001.
- In the Total Credits field, enter 100.
Before you click Add Household, what do you expect to see after submitting the form?
- Click Add Household.
The page refreshes with a green Household added successfully! banner. The Rakoto Family appears in the table below with credits showing 100 / 100 and a green OK badge.
- Add a second household to confirm the table grows. Enter Rabe Family as the name, your email, +261340000002 as the phone, and 50 credits.
- Click Add Household.
Both households now appear in the table, each with a green OK badge and full credit balances.
Try it on your phone
If your phone is on the same Wi-Fi network as your computer, open the admin page using your computer's local IP address (e.g. http://192.168.x.x:8080/admin). The Bootstrap layout adapts to smaller screens, so the form and table stack vertically.
Your admin dashboard is live and storing data. Next up, you will simulate electricity consumption so those credit balances start dropping.
Simulate Credit Consumption
Your admin dashboard can register households and display their credit balances. In the real world, those credits drain as people use electricity. Right now, every household sits at 100% forever.
In this step, you'll build a scheduled service that automatically deducts credits every 30 seconds, simulating real consumption. You'll also discover the core problem this project exists to solve: credits drain silently with no warning.
In this step, get ready to:
- Create a CreditConsumptionService that automatically deducts credits every 30 seconds.
- Observe credits draining on the admin dashboard and spot the gap: no one gets warned.
Create the credit consumption service
Spring's @Scheduled annotation lets you run a method on a timer without any external scheduling tools. Your application already has @EnableScheduling on the main class, so Spring is ready to pick up scheduled methods.
Why @Scheduled instead of a message queue?
For a single-server app that checks credit balances every 30 seconds, Spring's built-in @Scheduled is the right tool. A message queue like RabbitMQ or Kafka would add infrastructure complexity without improving anything at this scale.
- In IntelliJ IDEA, right-click the com.example.electricityalerts.service package in the Project sidebar. If the service package does not exist yet, right-click com.example.electricityalerts and select New then Package to create it.
- Select New, then Java Class.
- Name the class CreditConsumptionService and press Enter.
- Replace the entire contents of the new file with this class skeleton:
package com.example.electricityalerts.service;
import com.example.electricityalerts.model.Household;
import com.example.electricityalerts.repository.HouseholdRepository;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Random;
@Service
public class CreditConsumptionService {
// Repository for reading and saving household records
private final HouseholdRepository householdRepository;
// Random number generator for simulating variable electricity usage
private final Random random = new Random();
public CreditConsumptionService(HouseholdRepository householdRepository) {
this.householdRepository = householdRepository;
}
What does this code do?
This sets up the service shell. @Service tells Spring Boot to manage this class as a bean, and the constructor injects the HouseholdRepository so you can read and save household data.
The Random field generates different deduction amounts each cycle. This simulates unpredictable electricity consumption rather than a flat drain.
- Add the scheduled consumption method directly below the constructor, inside the class braces, then close the class with a final }:
// Runs automatically every 30 seconds (30000 milliseconds)
@Scheduled(fixedRate = 30000)
public void consumeCredits() {
List<Household> households = householdRepository.findAll();
for (Household household : households) {
// Only consume from households that still have credits
if (household.getRemainingCredits() > 0) {
// Deduct a random amount between 1 and 5
int consumption = random.nextInt(5) + 1;
int newBalance = Math.max(0, household.getRemainingCredits() - consumption);
household.setRemainingCredits(newBalance);
householdRepository.save(household);
}
}
}
}
What does this method do?
- @Scheduled(fixedRate = 30000) tells Spring to call consumeCredits() every 30 seconds automatically. No external cron job needed.
- random.nextInt(5) + 1 generates a random number between 1 and 5, so each household loses a different amount of credits each cycle.
- Math.max(0, ...) prevents credits from going negative. Once a household hits zero, consumption stops for that household.
- Save CreditConsumptionService.java by pressing Cmd+S (macOS) or Ctrl+S (Windows).
✔️ Awesome, I've got everything!
Great. Make sure you have saved CreditConsumptionService.java before moving on.
ⓧ I'd like to double check the full code
Scroll up and confirm your file has both parts in order:
- The class skeleton: package declaration, six imports, @Service annotation, class declaration with householdRepository and random fields, and the constructor.
- The consumeCredits() method with @Scheduled(fixedRate = 30000), the for loop deducting 1-5 credits, and the closing class brace.
Seeing red underlines in IntelliJ?
- Check that the package declaration at the top matches com.example.electricityalerts.service. If you created the file in the wrong folder, the package will not match and imports will fail.
- Make sure the class brace at the very end closes the CreditConsumptionService class. A missing } at the end causes cascading errors.
Help me fix import or package errors in my CreditConsumptionService.
Watch credits drain and spot the gap
The service is written, but the running application does not know about it yet. You need to restart the app so Spring picks up the new @Scheduled method.
What about my test households?
Because H2 is an in-memory database, all data is wiped when the application stops. After restarting, you will add a fresh test household to watch the consumption in action.
- Stop the running application by pressing Ctrl+C in the terminal where it is running.
- Restart the application by running this command:
mvn spring-boot:run
- Open http://localhost:8080/admin in your browser.
- Add a test household using the form. Enter any name, your email, any phone number, and set the credits to 50.
- Click Add Household.
You should see your household appear in the table with credits showing 50 / 50 and a green OK badge.
- Wait about 60 seconds.
Before you refresh, think about what you expect to see. The consumption service runs every 30 seconds and deducts 1-5 credits each time. After 60 seconds, that is two cycles. What should the remaining credits be?
- Refresh the admin page in your browser.
The remaining credits have dropped from 50. The exact number depends on the random deductions, but you should see something like 42 / 50 or 44 / 50. The status badge still shows green OK.
Now notice the problem. Those credits are ticking down toward zero, but nobody is being warned. No email, no alert, nothing. A household could run out of electricity and never see it coming.
This silent drain is intentional
The gap you are seeing is the exact problem this project solves. Credits drain, but no one gets notified before they hit zero. In the next step, you will wire up email alerts that fire automatically when credits drop below the 20% warning threshold.
Credits are not decreasing?
- Make sure you restarted the application after creating CreditConsumptionService.java. Spring does not hot-reload new classes by default.
- Confirm that @EnableScheduling is present on your ElectricityAlertsApplication class. Without it, Spring ignores all @Scheduled annotations.
- Wait a full 60 seconds before refreshing. The first cycle runs 30 seconds after startup, so a quick refresh may be too early.
Help me figure out why my scheduled credit consumption is not running.
Your system now simulates real electricity usage, with credits ticking down automatically. The silent drain is exactly the problem worth solving. In the next step, you will wire up email notifications that fire before credits hit zero.
Wire Up Email Notifications
In the last step, you watched credit balances drain every 30 seconds on your admin dashboard. The problem is clear: when a household's credits drop toward zero, nobody gets a warning.
This step fixes that gap. You will connect your app to Gmail SMTP so it can send real emails, build a notification service, and create a credit monitor that automatically fires alerts when balances run low.
In this step, get ready to:
- Configure Gmail SMTP with a secure App Password so your application can send emails.
- Build a notification service interface and its email implementation using JavaMailSender.
- Create a credit monitor that checks balances on a schedule and triggers alerts automatically.
Configure Gmail SMTP with your App Password
Your application.properties already has the Gmail SMTP settings from Step 1, but the username and password are still placeholders. To send real emails, you need a Gmail App Password.
What is a Gmail App Password?
An App Password is a 16-digit passcode that lets your Spring Boot application authenticate with Gmail's SMTP server. Google requires 2-Step Verification on your account before you can create one.
Your regular Gmail password will not work here. Google blocks third-party apps from using it directly.
- Open your browser and go to https://myaccount.google.com/apppasswords.
- Sign in to the Gmail account you want to send emails from, if prompted.
- In the App name field, type electricity-alerts.
- Click Create.
- Google displays a 16-digit passcode. Copy this passcode and save it somewhere safe. You will need it in the next step.
Don't see the App Passwords page?
App Passwords require 2-Step Verification to be enabled on your Google account. If you see a blank page or an error, go to your Google Account security settings and enable 2-Step Verification first, then return to the App Passwords page.
help me enable 2-Step Verification on my Google account so I can generate an App Password
Now update your SMTP configuration with your real credentials.
- Switch back to IntelliJ IDEA and open src/main/resources/application.properties.
- Find this line in the Gmail SMTP section:
spring.mail.username=your-email@gmail.com
- Replace your-email@gmail.com with your actual Gmail address.
- Find this line just below it:
spring.mail.password=your-app-password-here
- Replace your-app-password-here with the 16-digit App Password you just generated.
- Save application.properties.
Do not share your App Password
Your App Password grants full email access to your Gmail account. Never commit it to version control or share it publicly. Keep the placeholder values in any code you share.
Stop your running application in IntelliJ by pressing the red Stop button, then restart it to pick up the new credentials.
- Confirm the application starts without authentication errors in the console. You should see the usual Spring Boot startup banner with no stack traces mentioning mail.
Seeing an AuthenticationFailedException?
Double-check that you pasted the 16-digit App Password (not your regular Gmail password) into spring.mail.password. Make sure there are no extra spaces around it.
Also confirm that spring.mail.username is your full Gmail address including @gmail.com.
help me fix a Gmail SMTP AuthenticationFailedException in my Spring Boot application
Build the email notification service
With SMTP configured, the application can send emails. Now you need the code that decides what to send and how to send it.
You will start with a NotificationService interface. This defines a single sendAlert method that any notification channel (email, SMS, push) can implement.
- In IntelliJ, right-click the com.example.electricityalerts.service package and create a new Java interface called NotificationService.
- Add the following code:
package com.example.electricityalerts.service;
import com.example.electricityalerts.model.Household;
public interface NotificationService {
void sendAlert(Household household);
}
Why use an interface here?
The NotificationService interface defines a contract: any class that implements it must provide a sendAlert method. This makes it easy to swap or add notification channels later without changing the code that calls them.
- Save NotificationService.java.
Now create the email implementation that uses Spring's JavaMailSender to send the actual alert.
- In the same service package, create a new Java class called EmailNotificationService.
- Add the following code:
package com.example.electricityalerts.service;
import com.example.electricityalerts.model.Household;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
@Service
public class EmailNotificationService implements NotificationService {
private final JavaMailSender mailSender;
public EmailNotificationService(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
@Override
public void sendAlert(Household household) {
// Build a simple email message
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(household.getOwnerEmail());
message.setSubject("Low Electricity Credits Warning - " + household.getHouseholdName());
message.setText(
"Dear " + household.getHouseholdName() + " household,\n\n" +
"Your electricity credits are running low.\n\n" +
"Remaining credits: " + household.getRemainingCredits() + " out of " + household.getTotalCredits() + "\n" +
"Current level: " + String.format("%.1f", household.getRemainingPercentage()) + "%\n\n" +
"Please top up your credits as soon as possible to avoid a power cut.\n\n" +
"Electricity Credit Alert System"
);
// Send the email via Gmail SMTP
mailSender.send(message);
System.out.println("Email alert sent to: " + household.getOwnerEmail());
}
}
What does this code do?
The @Service annotation registers this class as a Spring bean. Spring automatically injects the JavaMailSender that was auto-configured from your application.properties SMTP settings.
The sendAlert method builds a SimpleMailMessage with the household's email, a warning subject line, and a body showing the remaining credits and percentage. It then sends the email and logs a confirmation to the console.
- Save EmailNotificationService.java.
- Restart the application to verify it still compiles and starts cleanly with both new files.
Application won't start?
Make sure EmailNotificationService has implements NotificationService in its class declaration. Without this, Spring cannot wire it as the interface implementation.
Also check that the import for Household points to com.example.electricityalerts.model.Household in both files.
help me fix compilation errors in my EmailNotificationService class
Create the credit monitor and test the full alert loop
The email service is ready, but nothing calls it yet. You need a scheduled monitor that checks every household's balance and fires an alert when credits drop below the warning threshold.
- In the service package, create a new Java class called CreditMonitorService.
- Add the following code:
package com.example.electricityalerts.service;
import com.example.electricityalerts.model.Household;
import com.example.electricityalerts.repository.HouseholdRepository;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CreditMonitorService {
private final HouseholdRepository householdRepository;
private final NotificationService notificationService;
public CreditMonitorService(HouseholdRepository householdRepository,
NotificationService notificationService) {
this.householdRepository = householdRepository;
this.notificationService = notificationService;
}
// Check balances every 30 seconds, offset 15 seconds from consumption
@Scheduled(fixedRate = 30000, initialDelay = 15000)
public void monitorCredits() {
List<Household> households = householdRepository.findByNotifiedFalse();
for (Household household : households) {
double percentage = household.getRemainingPercentage();
if (percentage <= household.getWarningThreshold()) {
notificationService.sendAlert(household);
household.setNotified(true);
householdRepository.save(household);
System.out.println("Alert triggered for: " + household.getHouseholdName()
+ " at " + String.format("%.1f", percentage) + "%");
}
}
}
}
What does this code do?
The @Scheduled(fixedRate = 30000, initialDelay = 15000) annotation runs monitorCredits() every 30 seconds, starting 15 seconds after the app launches. This offset prevents the monitor from running at the exact same moment as the consumption service.
The method queries only households that have not been notified yet using findByNotifiedFalse(). For each one, it checks if the remaining percentage is at or below the warning threshold (20% by default). If so, it sends an alert, marks the household as notified, and logs the event.
- Save CreditMonitorService.java.
- Stop and restart the application.
✔️ Awesome, I've got everything!
Great. Double check that you have saved all four files: application.properties, NotificationService.java, EmailNotificationService.java, and CreditMonitorService.java.
ⓧ I'd like to double check the full code
Here is the NotificationService.java interface:
package com.example.electricityalerts.service;
import com.example.electricityalerts.model.Household;
public interface NotificationService {
void sendAlert(Household household);
}
Here is the EmailNotificationService.java:
package com.example.electricityalerts.service;
import com.example.electricityalerts.model.Household;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Service;
@Service
public class EmailNotificationService implements NotificationService {
private final JavaMailSender mailSender;
public EmailNotificationService(JavaMailSender mailSender) {
this.mailSender = mailSender;
}
@Override
public void sendAlert(Household household) {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(household.getOwnerEmail());
message.setSubject("Low Electricity Credits Warning - " + household.getHouseholdName());
message.setText(
"Dear " + household.getHouseholdName() + " household,\n\n" +
"Your electricity credits are running low.\n\n" +
"Remaining credits: " + household.getRemainingCredits() + " out of " + household.getTotalCredits() + "\n" +
"Current level: " + String.format("%.1f", household.getRemainingPercentage()) + "%\n\n" +
"Please top up your credits as soon as possible to avoid a power cut.\n\n" +
"Electricity Credit Alert System"
);
mailSender.send(message);
System.out.println("Email alert sent to: " + household.getOwnerEmail());
}
}
Here is the CreditMonitorService.java:
package com.example.electricityalerts.service;
import com.example.electricityalerts.model.Household;
import com.example.electricityalerts.repository.HouseholdRepository;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CreditMonitorService {
private final HouseholdRepository householdRepository;
private final NotificationService notificationService;
public CreditMonitorService(HouseholdRepository householdRepository,
NotificationService notificationService) {
this.householdRepository = householdRepository;
this.notificationService = notificationService;
}
@Scheduled(fixedRate = 30000, initialDelay = 15000)
public void monitorCredits() {
List<Household> households = householdRepository.findByNotifiedFalse();
for (Household household : households) {
double percentage = household.getRemainingPercentage();
if (percentage <= household.getWarningThreshold()) {
notificationService.sendAlert(household);
household.setNotified(true);
householdRepository.save(household);
System.out.println("Alert triggered for: " + household.getHouseholdName()
+ " at " + String.format("%.1f", percentage) + "%");
}
}
}
}
Time to test the full alert loop. Before you do, think about this: if you add a household with only 10 credits and the consumption service deducts 1 to 5 credits every 30 seconds, what do you think will happen?
- Open your browser and go to http://localhost:8080/admin.
- In the Household Name field, enter a test name like Test Family.
- In the Owner Email field, enter the same Gmail address you configured in application.properties.
- In the Phone Number field, enter any placeholder like +261000000000.
- In the Total Credits field, enter 10.
- Click Add Household.
With only 10 credits, the balance will drop below the 20% warning threshold (2 credits) within a couple of consumption cycles.
- Wait about 2 minutes, then refresh the admin dashboard.
- Watch the Status column. Once credits drop below 20%, the status should change from the green OK badge to a yellow Low - Notified badge.
- Check your IntelliJ console. You should see log lines like:
Email alert sent to: your-email@gmail.com
Alert triggered for: Test Family at 10.0%
- Open your Gmail inbox. You should see an email with the subject line starting with Low Electricity Credits Warning and the body showing the household name and remaining credits.
No email in your inbox?
Check your Gmail Spam folder. Emails sent from your own address to yourself sometimes land there.
If there is no email at all, look at the IntelliJ console for error messages. A MailAuthenticationException means your App Password is incorrect. A connection timeout means your firewall may be blocking outbound traffic on port 587.
help me troubleshoot why my Spring Boot app is not sending Gmail emails
Your notification system is live. Households are being monitored, and email alerts fire the moment credits run low. Next up, you will take on an optional Secret Mission to add a second notification channel.
Secret mission
Add SMS Notifications with Twilio
Your alert system sends emails, but what about users who do not check their inbox? In this Secret Mission, you will integrate Twilio SMS as a second notification channel. You will create an SmsNotificationService behind the same NotificationService interface, wire it into your monitor, and watch both an email and an SMS land when credits run low.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- H2 in-memory database: automatically destroyed when the application stops. No cleanup needed.
- Gmail App Password: remains active on your Google account until you revoke it.
- Twilio trial account (Secret Mission only): free trial expires after 30 days. No credit card was required, so no charges will occur.
- electricity-alerts project folder: local files on your machine.
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with your alert system.
- The application runs locally and costs nothing to keep on your machine.
- The H2 database resets every time you restart the application, so you will start with a fresh set of households each run.
- Your Gmail App Password stays active but costs nothing.
- If you completed the Secret Mission, your Twilio trial account remains active for 30 days with no charges.
Pause - I'll come back to this later
Stop the running application to free up your terminal. Your project files stay on your machine, ready to pick up where you left off.
- Stop the application by pressing Ctrl+C in the terminal where it is running, or click the Stop button in IntelliJ IDEA.
- The H2 database data is cleared automatically when the application stops. Your code and configuration files remain intact.
- To resume, reopen the project in IntelliJ IDEA and run the application again.
Delete - I don't want to use this again
Remove all project resources and credentials. Follow each item below to fully clean up.
- Stop the running application by pressing Ctrl+C in the terminal, or click the Stop button in IntelliJ IDEA.
- Revoke your Gmail App Password by visiting https://myaccount.google.com/apppasswords.
- Find the App Password you created for this project and click Revoke next to it.
- If you completed the Secret Mission, you can delete your Twilio trial account by visiting console.twilio.com and navigating to your account settings. The trial expires automatically after 30 days with no charges, so this is optional.
- Delete the electricity-alerts project folder from your machine.
On macOS or Linux, run this command from the folder that contains your project:
rm -rf electricity-alerts
On Windows, run this command from the folder that contains your project:
rmdir /s /q electricity-alerts
Nice Work!
Nice Work!
Nice work! You just built a full prepaid electricity credit alert system from scratch using Spring Boot. Households get registered, credits drain automatically, and email warnings fire off before anyone loses power.
You've learned how to:
- Build a Spring Boot 4.1.0 web application with a responsive admin dashboard using Thymeleaf, Bootstrap, Spring Data JPA, and an H2 in-memory database to manage household electricity credits from any device.
- Simulate automatic credit consumption and monitoring using Spring's @Scheduled annotation, with offset timers that run consumption and alerting on independent cycles.
- Wire up an email notification system using JavaMailSender and Gmail SMTP that automatically alerts users when their credits drop below the 20% warning threshold.
- Secret Mission: integrate Twilio SMS as a second notification channel behind the same NotificationService interface, sending alerts to Madagascar phone numbers (+261).
Ready to quiz yourself?