Let’s be honest: searching for a job is a full-time job in itself. 😩
Most of us rely on messy spreadsheets, generic Trello boards, or just "hoping for the best" to track applications. I wanted something better. I wanted a tool that didn't just list jobs, but actually visualized my pipeline, calculated my interview conversion rates, and felt like a modern SaaS product.
So, I built JobTrackPro.
But as an engineer, I didn't want to just build a simple CRUD app. I treated this as a System Design Challenge. I focused on security, concurrency, and cloud-native storage patterns.
Here is a deep dive into how I architected this Enterprise-Grade application using Spring Boot 3 (Java 21) and Angular 17.
🔗 The Goods
Before we dive into the code, you can try it yourself
- 🚀 Live Demo: https://thughari.github.io/JobTrackerPro-UI
- 💻 Backend Repo: github.com/thughari/JobTrackerPro
- 🎨 Frontend Repo: github.com/thughari/JobTrackerPro-UI
🛠️ The Tech Stack
I chose a stack that balances raw performance with developer experience:
- Backend: Java 21 + Spring Boot 3.2
- Security: Spring Security 6 (OAuth2 + JWT)
- Database: PostgreSQL (Production) / MySQL (Dev)
- Storage: Cloudflare R2 (AWS S3 Compatible)
- Email: Spring Mail (SMTP) + Brevo
- Frontend: Angular 17 (Signals) + TailwindCSS + D3.js
- DevOps: Docker + Google Cloud Run + GitHub Actions
🧠 Architectural Deep Dive
Here are the four most interesting engineering challenges I solved while building this.
1. Hybrid Authentication (Google/GitHub + Local)
Most tutorials show you how to do either OAuth2 or JWT/Password auth. Doing both in the same system is tricky.
The Goal: Allow users to sign in with Google/GitHub, but also let them set a password later if they want to detach their account.
The Flow:
I implemented a custom OAuth2SuccessHandler. When a user logs in via Google:
- We check if the email exists.
- If not, we create the user on the fly.
- We download their avatar URL and sync it to our system.
- We issue a JWT so the frontend remains stateless.
@Component
public class OAuth2SuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(...) {
// Logic to extract Google vs GitHub attributes
OAuth2User oAuth2User = authToken.getPrincipal();
String registrationId = authToken.getAuthorizedClientRegistrationId();
UserInfo userInfo = extractUserInfo(registrationId, oAuth2User.getAttributes());
// Upsert User logic...
// Redirect to Angular with Token
getRedirectStrategy().sendRedirect(request, response, uiUrl + "/login-success?token=" + token);
}
}
2. Multi-threaded Analytics with CompletableFuture
The dashboard shows four key metrics: Total Applications, Active Pipeline, Interviews, and Offers.
Querying these sequentially would be slow as the dataset grows. Instead of hitting the database 4 times in a row, I used Java's CompletableFuture to run these aggregations in parallel threads.
This reduced the dashboard load time significantly.
// Running database aggregations in parallel
CompletableFuture<Long> activeFuture = CompletableFuture.supplyAsync(() ->
jobRepository.countActiveJobs(email), executor
);
CompletableFuture<Long> interviewFuture = CompletableFuture.supplyAsync(() ->
jobRepository.countInterviews(email), executor
);
// Wait for all to complete
CompletableFuture.allOf(activeFuture, interviewFuture).join();
3. Atomic Profile Updates & Cloudflare R2
Storing images in a database (BLOBs) bloats backups. Storing them on disk doesn't work in serverless environments (like Cloud Run).
I integrated Cloudflare R2 using the AWS SDK v2.
The tricky part was Data Integrity. I wanted users to preview an image instantly, but not "save" it until they clicked the Save button.
- Frontend: Uses
FileReaderto show a local preview instantly (Zero latency). - Backend: Handles the File Upload and Text Update in a Single Atomic Transaction.
- Cleanup: If the user replaces an image, the backend automatically sends a
deleteObjectcommand to R2 to remove the old file, preventing "orphaned files" from costing money.
4. Async Email System
For the "Forgot Password" flow, I didn't want the user to wait for the SMTP handshake (which can take 2-3 seconds).
I used Spring's @Async annotation to offload email sending to a background thread. The API returns "Reset link sent" immediately, ensuring a snappy UI experience.
⚡ Angular Signals & Optimistic UI
On the frontend, I went all-in on Angular 17 Signals.
The entire application state (Jobs list, Stats, Profile data) is reactive. This allows for Optimistic UI Updates.
The "Magic" Moment:
When you add a job, the UI updates the Signal list immediately. The POST request fires in the background. The Dashboard charts re-render instantly without waiting for a server round-trip.
// Angular Service: Optimistic Update
async addJob(job: any) {
// 1. Send Request
await firstValueFrom(this.http.post(this.apiUrl, job));
// 2. Trigger smart refresh (Updates signals immediately)
this.refreshActiveView();
}
🏁 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 what happens if an image upload fails, how to secure OAuth2 redirects, and how to make analytics scalable.
I’m keeping this project Open Source to help other developers who might be building similar dashboard applications.
Feel free to fork it, star it, or use it to land your next job! 👇
- Live App: https://thughari.github.io/JobTrackerPro-UI
- Backend Source: thughari/JobTrackerPro
- Frontend Source: thughari/JobTrackerPro-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! 🚀

Top comments (0)