Spring Boot Microservices Project: Home Energy Tracker — Insight Service, Ollama, & Local LLM Integration (Part 8)
1. Why Do We Need an Insight Service? (Problem Statement & Status Quo)
As our Home Energy Tracker microservices architecture matures, we have successfully implemented authentication, device management, telemetry ingestion, alert dispatching, and time-series aggregation via InfluxDB. However, raw numeric data and statistical tables are notoriously difficult for everyday end-users to interpret.
To bridge the gap between raw telemetry and actionable human intelligence, we need a dedicated Insight Service. This service acts as the cognitive layer of our system:
- It aggregates multi-day energy consumption metrics across all devices owned by a user.
- It interfaces with an AI Large Language Model (LLM) running locally on the developer's machine.
- It formulates structured prompt engineering templates to request personalized energy-saving recommendations and behavioral insights.
2. Core Mental Model: The AI Advisory Workflow
Think of the Insight Service as an expert energy consultant sitting in an office. When a homeowner walks in asking for ways to lower their monthly electricity bill, the consultant does not guess.
Instead, the consultant calls the Usage Department (Usage Service) to pull historical utility records for the past 3 days, compiles all active appliances (Thermostats, Doorbells, Speakers), calculates cumulative kilowatt-hours, and reviews the ledger before sitting down with an AI assistant to draft a custom optimization report.
3. Spring Boot Version Selection Strategy (Spring Boot 3.5.9 vs. 4.0.1)
When creating the inside-service on Spring Initializr, a deliberate architectural choice is made regarding the framework version:
- Spring Boot 4.0.1 (Status Quo): The bleeding-edge version powering our other microservices. However, native Spring AI modules for Ollama integration are not yet fully stabilized or supported out-of-the-box in early Spring Boot 4 distributions.
- Spring Boot 3.5.9 (Selected Strategy): Chosen specifically for the
inside-servicebecause it includes native, first-class support forspring-ai-starter-ollama, enabling seamless integration with local LLM beans without resorting to brittle customRestTemplatewrappers around local HTTP sockets.
4. Setting Up the Insight Service & DTO Architecture
We configure the inside-service project using Maven, Java 21, and Spring Boot 3.5.9.
application.properties Configuration
server.port=885
spring.application.name=inside-service
# Downstream Service Discovery Endpoints
usage.service.url=http://localhost:883/api/v1/usage
# Spring AI Ollama Configuration
spring.ai.ollama.base-url=http://localhost:11434
spring.ai.ollama.chat.options.model=deepseek-coder
spring.ai.ollama.chat.options.pull-model-strategy=never
Immutable Record DTOs (InsideDTO, UsageDTO, DeviceDTO)
Because Java records provide immutability and thread safety, we use them extensively for Data Transfer Objects, combined with Lombok @Builder annotations.
// package leadjourney.dto;
@Builder
public record InsideDTO(
Long userId,
String tips,
Double energyUsage
) {}
// package leadjourney.dto;
@Builder
public record UsageDTO(
Long userId,
List<DeviceDTO> devices
) {}
// package leadjourney.dto;
@Builder
public record DeviceDTO(
Long id,
String name,
String type,
String location,
Double energyConsumed
) {}
5. Building the Insight Controller
The InsideController exposes two primary HTTP GET endpoints: one for targeted energy-saving tips, and a more comprehensive overview endpoint that evaluates the last three days of usage data.
package leadjourney.controller;
import leadjourney.dto.InsideDTO;
import leadjourney.service.InsideService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/v1/insights")
@RequiredArgsConstructor
public class InsideController {
private final InsideService insideService;
@GetMapping("/savings-tips/{userId}")
public ResponseEntity<InsideDTO> getSavingsTips(@PathVariable Long userId) {
InsideDTO response = insideService.getSavingTipsForUser(userId);
return ResponseEntity.ok(response);
}
@GetMapping("/overview/{userId}")
public ResponseEntity<InsideDTO> getOverview(@PathVariable Long userId) {
InsideDTO response = insideService.getOverview(userId);
return ResponseEntity.ok(response);
}
}
6. Implementing Inter-Service Communication (UsageClient)
The InsideService must pull aggregated telemetry from the UsageService. We implement a custom component using Spring's RestTemplate and UriComponentsBuilder.
package leadjourney.client;
import leadjourney.dto.UsageDTO;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
@Component
@RequiredArgsConstructor
public class UsageClient {
private final RestTemplate restTemplate;
@Value("${usage.service.url}")
private String baseUsageUrl;
public UsageDTO getXDaysUsageForUser(Long userId, int days) {
String url = UriComponentsBuilder.fromUriString(baseUsageUrl)
.path("/{userId}")
.queryParam("days", days)
.buildAndExpand(userId)
.toUriString();
ResponseEntity<UsageDTO> response = restTemplate.getForEntity(url, UsageDTO.class);
return response.getBody();
}
}
7. Extending the Usage and Device Services
To fulfill requests from the Insight Service, the Usage Service needs an aggregation endpoint (GET /api/v1/usage/{userId}?days=3), which in turn depends on the Device Service returning all devices belonging to a specific user.
Device Repository Extension (DeviceRepository)
package leadjourney.repository;
import leadjourney.model.Device;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface DeviceRepository extends JpaRepository<Device, Long> {
List<Device, Long> findAllByUserId(Long userId);
}
Usage Service Aggregation Logic & InfluxDB Flux Queries
In the UsageService, we query InfluxDB for time-series energy consumption metrics across all device IDs owned by the target user, grouping and summing the results over the specified window (e.g., past 3 days).
// Simplified core loop within UsageServiceImpl
public UsageDTO getXDaysUsageForUser(Long userId, int days) {
List<DeviceDTO> devices = deviceClient.getAllDevicesForUser(userId);
if (devices == null || devices.isEmpty()) {
return UsageDTO.builder().userId(userId).devices(List.of()).build();
}
List<String> deviceIdStrings = devices.stream()
.map(DeviceDTO::getId)
.filter(Objects::nonNull)
.map(String::valueOf)
.toList();
Instant now = Instant.now();
Instant start = now.minusSeconds(days * 24L * 3600L);
// Build Flux query dynamically filtering on target device IDs
String deviceFilter = deviceIdStringJoiner(deviceIdStrings);
String fluxQuery = String.format("""
from(bucket: "energy-bucket")
|> range(start: %s, stop: %s)
|> filter(fn: (r) => r["_measurement"] == "energy_consumption")
|> filter(fn: (%s))
|> group(columns: ["device_id"])
|> sum(column: "_value")
""", start.toString(), now.toString(), deviceFilter);
Map<Long, Double> aggregatedMap = executeFluxQuery(fluxQuery);
// Populate energy consumption on mutable domain models before returning DTOs
// ...
}
8. Integrating Local LLMs via Spring AI & Ollama
With aggregated usage data retrieved from the UsageService, the InsideService constructs an intelligent prompt and submits it to Ollama running locally on port 11434.
Ollama Chat Client Configuration
package leadjourney.config;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OllamaConfig {
@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder
.defaultSystem("You are an expert energy efficiency advisor. Provide concise, practical advice to users on how to reduce their energy consumption based on their usage patterns.")
.build();
}
}
AI Prompt Execution & Insight Generation (InsideService)
package leadjourney.service;
import leadjourney.client.UsageClient;
import leadjourney.dto.InsideDTO;
import leadjourney.dto.UsageDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
@Slf4j
@RequiredArgsConstructor
public class InsideService {
private final UsageClient usageClient;
private final ChatClient chatClient;
public InsideDTO getOverview(Long userId) {
log.info("Fetching energy usage overview for user ID: {} over past 3 days", userId);
UsageDTO usageDTO = usageClient.getXDaysUsageForUser(userId, 3);
double totalUsage = usageDTO.devices().stream()
.mapToDouble(d -> d.energyConsumed() != null ? d.energyConsumed() : 0.0)
.sum();
String prompt = String.format("""
Analyze the following energy usage data and provide a concise overview with actionable insights.
This data represents aggregated metrics for the past 3 days.
Total Energy Consumption: %.2f kWh.
Detailed Device Breakdown: %s
""", totalUsage, usageDTO.devices().toString());
log.info("Calling local Ollama LLM for user ID: {} with total usage: {}", userId, totalUsage);
String aiResponse = chatClient.prompt()
.user(prompt)
.call()
.content();
return InsideDTO.builder()
.userId(userId)
.tips(aiResponse)
.energyUsage(totalUsage)
.build();
}
public InsideDTO getSavingTipsForUser(Long userId) {
// Targeted optimization prompt logic
return getOverview(userId);
}
}
9. Real-World Practical Pitfalls & Debugging Mindset
- Immutability Conflicts with Records: Java records are immutable. When querying InfluxDB and handling exceptions where energy consumption defaults to
0.0, mutating fields directly on a record throws compilation errors. Fix: Map database query results into mutable domain model objects first, calculate sums, and only then project them into immutable record DTOs. - Ollama Startup & Model Pull Delays: If Ollama is not running locally (
http://localhost:11434), or if the requested model (deepseek-coder) hasn't been pulled into local storage, REST calls will time out with 500 Internal Server Errors. Always ensure Ollama is active via terminal (ollama serve) and the model is pre-downloaded. - Simulator Data Range: Ensure the data simulator in the ingestion service generates telemetry for device IDs that match actual user device ownership records in PostgreSQL (e.g., expanding device simulation range from
1-6to1-200).
10. Complete Microservices Port & Communication Reference
11. Summary and Key Takeaways
- Cognitive Microservice Layer: The
Insight Servicesuccessfully aggregates raw telemetry and translates it into human-readable advice using generative AI. - Spring Boot Version Isolation: We selectively used Spring Boot
3.5.9for theinside-serviceto leverage mature, nativeSpring AIstarters for Ollama integration. - Local LLM Execution: By integrating with Ollama running locally (
deepseek-codermodel on port11434), we avoided external cloud API costs and maintained data privacy. - Inter-Service Data Flow: The Insight Service queries the Usage Service, which orchestrates calls to the Device Service and InfluxDB to assemble a complete user energy profile.
- Immutable Record Safety: Utilizing Java records for DTOs ensures payload integrity, requiring clean mapping between mutable domain entities and immutable response structures.
- End-to-End Verification: Testing via Postman (
GET /api/v1/insights/overview/1) successfully invokes local LLM inference, returning structured optimization insights based on real aggregated usage metrics.