I stopped using Excel: How I built an Enterprise-Grade Job Tracker with Spring Boot 3 & Angular 17
Source: Dev.to
Note: This isn’t just a simple CRUD app. I treated it as a System Design Challenge, focusing on security, concurrency, and cloud‑native storage patterns.
🔗 The Goods
- Live Demo – https://thughari.github.io/JobTrackPro-UI
- Backend Repo – https://github.com/thughari/JobTrackPro
- Frontend Repo – https://github.com/thughari/JobTrackPro-UI
🛠️ Tech Stack
| Layer | Technology |
|---|---|
| Backend | Java 21 + Spring Boot 3.2 |
| Security | Spring Security 6 (OAuth2 + JWT) |
| Database | PostgreSQL (production) / MySQL (dev) |
| Storage | Cloudflare R2 (AWS S3‑compatible) |
| Spring Mail (SMTP) + Brevo | |
| Frontend | Angular 17 (Signals) + TailwindCSS + D3.js |
| DevOps | Docker + Google Cloud Run + GitHub Actions |
🧠 Architectural Deep Dive
Below are the four most interesting engineering challenges I solved while building JobTrackPro.
1️⃣ Hybrid Authentication (Google/GitHub + Local)
Goal – Let users sign in with Google or GitHub and later set a password to detach the external account.
Flow

Implemented a custom OAuth2SuccessHandler that:
- Checks if the email already exists.
- Creates the user on the fly if needed.
- Syncs the avatar URL.
- Issues a JWT for a stateless frontend.
@Component
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException {
// Extract provider‑specific attributes
OAuth2AuthenticationToken authToken = (OAuth2AuthenticationToken) authentication;
OAuth2User oAuth2User = authToken.getPrincipal();
String registrationId = authToken.getAuthorizedClientRegistrationId();
UserInfo userInfo = extractUserInfo(registrationId, oAuth2User.getAttributes());
// Upsert user (create if absent, update otherwise)
User user = userService.upsertUser(userInfo);
// Generate JWT
String token = jwtService.createToken(user);
// Redirect back to Angular with the token
String redirectUrl = uiUrl + "/login-success?token=" + token;
getRedirectStrategy().sendRedirect(request, response, redirectUrl);
}
}
2️⃣ Multi‑Threaded Analytics with CompletableFuture
The dashboard shows four key metrics. Running the queries sequentially became a bottleneck, so I parallelized them:
// Run aggregations in parallel
CompletableFuture<Long> activeFuture = CompletableFuture.supplyAsync(
() -> jobRepository.countActiveJobs(email), executor);
CompletableFuture<Long> interviewFuture = CompletableFuture.supplyAsync(
() -> jobRepository.countInterviews(email), executor);
CompletableFuture<Long> offerFuture = CompletableFuture.supplyAsync(
() -> jobRepository.countOffers(email), executor);
// Wait for all to finish
CompletableFuture.allOf(activeFuture, interviewFuture, offerFuture).join();
Long activeCount = activeFuture.join();
Long interviewCount = interviewFuture.join();
Long offerCount = offerFuture.join();
3️⃣ Atomic Profile Updates & Cloudflare R2
Storing images as BLOBs inflates backups, while local disks don’t work on serverless platforms. The solution uses Cloudflare R2 with a single atomic transaction:
@Transactional
public void updateProfile(ProfileDto dto, MultipartFile avatar) {
// 1. Upload new avatar to R2 (if present)
String newAvatarUrl = null;
if (avatar != null && !avatar.isEmpty()) {
newAvatarUrl = r2Service.upload(avatar);
}
// 2. Update profile fields
User user = userRepository.findById(dto.getUserId())
.orElseThrow(() -> new NotFoundException("User not found"));
user.setFullName(dto.getFullName());
user.setBio(dto.getBio());
// 3. Replace avatar URL atomically
if (newAvatarUrl != null) {
String oldAvatarUrl = user.getAvatarUrl();
user.setAvatarUrl(newAvatarUrl);
// Delete old file after successful DB commit
if (oldAvatarUrl != null) {
r2Service.delete(oldAvatarUrl);
}
}
userRepository.save(user);
}
4️⃣ Async Email System
The “Forgot Password” flow now returns instantly while the email is sent in the background:
@Service
public class EmailService {
@Async
public void sendPasswordReset(String to, String resetLink) {
SimpleMailMessage message = new SimpleMailMessage();
message.setTo(to);
message.setSubject("Password Reset Request");
message.setText("Click the link to reset your password: " + resetLink);
mailSender.send(message);
}
}
⚡ Angular Signals & Optimistic UI
The frontend relies entirely on Angular 17 Signals. State (jobs list, stats, profile) is reactive, enabling optimistic UI updates.
“Magic” Moment – When a job is added, the UI updates the Signal list immediately while the POST request runs in the background. Dashboard charts re‑render instantly.
// Angular service: optimistic update
async addJob(job: Job) {
// 1️⃣ Optimistically update the local signal
const current = this.jobsSignal();
this.jobsSignal.set([...current, job]);
// 2️⃣ Send request to backend
try {
const saved = await firstValueFrom(this.http.post(`${this.apiUrl}/jobs`, job));
// Replace the optimistic entry with the server‑generated one (e.g., ID)
this.jobsSignal.update(list => list.map(j => (j.tempId === job.tempId ? saved : j)));
} catch (err) {
// Rollback on error
this.jobsSignal.update(list => list.filter(j => j.tempId !== job.tempId));
throw err;
}
}
🎉 Final Thoughts
JobTrackPro shows how a well‑architected, cloud‑native stack can turn a personal productivity tool into an enterprise‑grade application. Key takeaways:
- Hybrid OAuth2/JWT authentication
- Parallel analytics with
CompletableFuture - Atomic media handling on Cloudflare R2
- Asynchronous email delivery
- Reactive Angular Signals
The combination delivers a fast, secure, and delightful user experience—exactly what a modern job‑search workflow deserves. Feel free to explore the demo, star the repos, or open an issue if you have suggestions! 🚀
🏁 Conclusion
Building JobTrackPro taught me that “Full Stack” isn’t just about connecting a database to a frontend. It’s about handling edge cases—like image‑upload failures, securing OAuth2 redirects, and making analytics scalable. The project remains Open Source to help developers building similar dashboards.
- Live App: https://thughari.github.io/JobTrackPro-UI
- Backend Source: https://github.com/thughari/JobTrackPro
- Frontend Source: https://github.com/thughari/JobTrackPro-UI
💬 Discussion
Have you tried using Cloudflare R2 for image storage yet? I found it significantly cheaper than S3. Let me know your thoughts on the architecture in the comments!
Thanks for reading! Happy coding! 🚀