I stopped using Excel: How I built an Enterprise-Grade Job Tracker with Spring Boot 3 & Angular 17

Published: (December 23, 2025 at 05:59 AM EST)
4 min read
Source: Dev.to

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

🛠️ Tech Stack

LayerTechnology
BackendJava 21 + Spring Boot 3.2
SecuritySpring Security 6 (OAuth2 + JWT)
DatabasePostgreSQL (production) / MySQL (dev)
StorageCloudflare R2 (AWS S3‑compatible)
EmailSpring Mail (SMTP) + Brevo
FrontendAngular 17 (Signals) + TailwindCSS + D3.js
DevOpsDocker + 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

Hybrid Authentication 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.

💬 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! 🚀

Back to Blog

Related posts

Read more »