A deep dive into the architecture, security decisions, and engineering trade-offs behind a medication reminder system built for real-world use.
The Problem
Medication non-adherence is a genuine health risk — especially for elderly patients or anyone managing multiple prescriptions. The challenge isn't just forgetting to take medicine; it's that there's no easy feedback loop for caretakers to know when a dose has been missed.
I wanted to build something that:
- Automatically reminds patients on schedule
- Alerts caretakers when a dose is missed — without them having to check manually
- Gives patients AI-powered drug information on demand
- Tracks long-term adherence with visual analytics
The result: MediRemind — a full-stack web app built with Spring Boot, React, MySQL, and Groq's Llama 3.3 API.
Tech Stack
| Layer | Technology |
|---|---|
| Frontend | React (Vite), Tailwind CSS, Recharts, Lucide React |
| Backend | Java 21, Spring Boot 3, Spring Security + JWT |
| Spring Mail (SMTP) | |
| Database | MySQL + Hibernate/JPA |
| AI | Groq API (Llama 3.3-70b via Llama3-8b-8192) |
| Testing | JUnit 5, Mockito, MockMvc |
Architecture Highlights
These are the three decisions I'm most proud of — and the ones I'd highlight in any technical interview.
1. 🔐 Secure Backend-Proxied AI Integration
Early in development, I made the classic mistake: calling the Groq API directly from the React frontend. It worked — but the API key was visible in every network request. Anyone opening DevTools could steal it.
The fix: I created a dedicated Spring Boot endpoint that acts as a proxy:
@RestController
@RequestMapping("/api/ai")
public class AiController {
private final AiService aiService;
public AiController(AiService aiService) {
this.aiService = aiService;
}
@GetMapping("/medicine-info")
public ResponseEntity<ApiResponse<String>> getMedicineInfo(@RequestParam String medicineName) {
String info = aiService.getMedicineInfo(medicineName);
return ResponseEntity.ok(ApiResponse.success("AI info retrieved successfully", info));
}
}
The frontend now sends a request to /api/ai/medicine-info?medicineName=Aspirin — authenticated with the user's JWT. The Groq key stays locked on the server. Simple fix, significant security improvement.
2. ⏰ Automated Cron Schedulers & Caretaker Alerts
The core of MediRemind's value is its background scheduling. Using Spring Boot's @Scheduled annotation in my ReminderScheduler class, I built four automated jobs:
@Component
public class ReminderScheduler {
// Every minute — send reminders for due doses and create logs
@Scheduled(fixedRate = 60000)
@Transactional
public void sendMedicineReminders() { ... }
// Every hour at :30 — alert caretakers about missed doses (older than 30 mins)
@Scheduled(cron = "0 30 * * * *")
@Transactional
public void sendMissedDoseAlerts() { ... }
// Every Sunday at 8AM — dispatch weekly adherence reports
@Scheduled(cron = "0 0 8 * * SUN")
@Transactional
public void sendWeeklyReports() { ... }
// Every midnight — deactivate expired medicines and complete past appointments
@Scheduled(cron = "0 0 0 * * *")
@Transactional
public void cleanupExpiredRecords() { ... }
}
The caretaker alert logic has an intentional delay: it only fires for doses that are still marked as MISSED and are 30+ minutes past their scheduled time. This prevents false alarms for patients who are slightly late but not actually non-adherent.
3. 🛠️ The Secure Localhost-Only Developer Console
Here's the engineering problem nobody talks about: how do you demo a time-based feature during a presentation?
Waiting for midnight to show the cleanup job isn't practical. Waiting an hour for the caretaker alert isn't either. My solution was a Developer Console — a UI panel that lets you manually trigger any scheduled job on demand.
The key engineering requirement: it must be completely invisible and disabled in production.
Frontend auto-detect check:
const isLocal = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
return (
<>
{isLocal && <DeveloperConsole />}
</>
);
Backend Profile protection:
@RestController
@RequestMapping("/api/jobs")
@Profile({"default", "dev"}) // Only loads in local/dev configurations
public class JobController {
private final ReminderScheduler reminderScheduler;
public JobController(ReminderScheduler reminderScheduler) {
this.reminderScheduler = reminderScheduler;
}
@PostMapping("/trigger-cleanup")
public ResponseEntity<ApiResponse<String>> triggerCleanup() {
reminderScheduler.cleanupExpiredRecords();
return ResponseEntity.ok(ApiResponse.success("Cleanup completed", null));
}
}
With the active Spring Profile set to prod in production, the entire JobController is absent from the application context. The endpoints return 404 Not Found. Even if someone knew the URL, there's nothing to call.
This is a pattern I'd use in any project that has background jobs — it makes local development, testing, and demo presentations dramatically smoother.
Testing Strategy
I focused on two areas: scheduler logic and API contracts.
Scheduler Unit Tests with Mockito
The tricky part of testing schedulers is that the business rules involve timing. I used Mockito to mock the repository layer and feed it controlled data — simulating scenarios like "dose was due 40 minutes ago and still hasn't been marked taken."
@ExtendWith(MockitoExtension.class)
public class ReminderSchedulerTest {
@Mock
private MedicineLogRepository medicineLogRepository;
@Mock
private ObserverService observerService;
@Mock
private EmailService emailService;
@InjectMocks
private ReminderScheduler reminderScheduler;
@Test
public void testSendMissedDoseAlerts_SendsAlert() {
// Arrange: Prepare a missed dose log from 40 minutes ago
MedicineLog log = buildMissedLog(LocalDateTime.now().minusMinutes(40));
when(medicineLogRepository.findMissedLogsOlderThan(any())).thenReturn(List.of(log));
when(observerService.getObserverEmailsForPatient(anyLong(), any())).thenReturn(List.of("guardian@example.com"));
// Act: Run missed dose routine
reminderScheduler.sendMissedDoseAlerts();
// Assert: Verify alerts are dispatched and observerAlerted is updated to true
assertTrue(log.getObserverAlerted());
verify(emailService, times(1)).sendObserverMissedDoseAlert(any(), any(), any(), any(), any());
verify(medicineLogRepository, times(1)).save(log);
}
}
Controller Integration Tests with MockMvc
I used MockMvc to verify that the Web API endpoints map correctly, and that our custom GlobalExceptionHandler returns standard JSON error responses — critical for a frontend that needs to display user-friendly error messages.
@WebMvcTest(controllers = DashboardController.class)
@AutoConfigureMockMvc(addFilters = false) // Bypasses security filter chain for WebMVC tests
public class DashboardControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private DashboardService dashboardService;
@MockBean
private JwtUtil jwtUtil;
@MockBean
private CustomUserDetailsService userDetailsService;
@Test
public void testGetDashboard_Success() throws Exception {
DashboardResponse mockResponse = DashboardResponse.builder()
.userName("John Doe")
.totalMedicines(5)
.build();
when(dashboardService.getDashboard()).thenReturn(mockResponse);
mockMvc.perform(get("/api/dashboard").contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.userName").value("John Doe"))
.andExpect(jsonPath("$.data.totalMedicines").value(5));
}
}
Key Takeaways
1. Never expose API keys on the client side. Even for a hobby project. Backend proxying is a one-time setup that eliminates an entire class of security vulnerabilities.
2. Background jobs need developer tooling. A cron job that runs at midnight is nearly impossible to test manually unless you build the tooling to trigger it on demand. The Localhost-Only Dev Console pattern is something I'll carry into every future project.
3. Mock at the database boundary, not the implementation. Mocking repositories rather than internal service methods kept my tests resilient to refactoring — the tests break only when the business logic actually changes, not when I rename a service class method.
4. Role-based security is worth the setup cost. Implementing JWT + Spring Security with separate PATIENT and CARETAKER roles early on made every feature downstream much cleaner to reason about.
What's Next
- Push notifications (Web Push API) as an alternative to email
- Mobile-first PWA support
- Dockerize the full stack for one-command local setup
If you found this useful or have questions about any of the architecture decisions — drop a comment. I'm also open to feedback on the testing approach; Mockito-heavy unit tests vs. more integration-focused tests is a tradeoff I'm still thinking through.
GitHub repo link in my profile. 🚀
Top comments (0)