Week 14

Architecture Patterns

Communication Patterns

Background Processing

Architectural Trade-offs

Messaging

Hands-on: RabbitMQ

Practice

Assignment

Backend Track

Introduction

Some work should simply not happen while a user is waiting: sending emails, generating reports, cleaning up old data. Work that runs outside the request/response cycle is called background processing. Spring Boot ships with two easy tools for it — @Async and @Scheduled — and this chapter shows how to use them and where they stop being enough.

@Async — respond now, work later

Marking a method with @Async makes Spring run it on a separate thread, so the caller continues immediately. Enable it once on your application class:

@SpringBootApplication
@EnableAsync
public class Application { }

Then mark the slow method:

@Service
@Slf4j
public class NotificationService {

    @Async
    public void sendWelcomeEmail(String email) {
        log.info("Sending welcome email to {}", email);
        // slow work: talking to a mail provider...
    }
}

When a controller calls notificationService.sendWelcomeEmail(email), the call returns instantly and the email is sent in the background — exactly the registration example from the previous chapter.

<aside> ⚠️

@Async only works when the method is called from another class. Spring wraps your bean in a proxy to redirect the call to a new thread — calling an @Async method from inside the same class skips the proxy and runs it like a normal method. A classic surprise.

</aside>

@Scheduled — run on a timer

For periodic work, enable scheduling with @EnableScheduling and annotate a method with @Scheduled. In Week 10 you learned that session tokens should have an expiry date — and someone has to clean the expired ones up:

@Component
@Slf4j
public class SessionCleanupJob {

    @Scheduled(cron = "0 0 3 * * *") // every day at 03:00
    public void deleteExpiredSessions() {
        log.info("Deleting expired sessions...");
        // DELETE FROM sessions WHERE expires_at < now()
    }
}

<aside> 💡

Spring cron expressions have six fields, starting with seconds: second minute hour day-of-month month day-of-week.

</aside>

@Scheduled has three modes:

Mode Meaning Typical use
cron Calendar schedule ("every day at 03:00") Nightly cleanup
fixedRate Start every N ms, measured start-to-start Polling a status every 30 seconds
fixedDelay Start N ms after the previous run finished Batch work — the safest default, a run can never overlap itself

Where these tools stop being enough

Both tools live inside your running application — and that is exactly their weakness:

  1. A restart or deploy loses work. A task that is running in the middle of a deploy simply disappears. Nothing re-runs it.
  2. There is no retry. If the method throws, the exception is logged and the work is not done. No second attempt.
  3. @Scheduled duplicates across instances. In production, applications usually run as two or more identical copies for reliability. Each copy has its own scheduler, so the 03:00 job fires on every copy. With session cleanup that is harmless — with "email the weekly report to all customers" it very much is not.

<aside> ❗

@Async and @Scheduled are excellent for a single-instance application. The moment work must survive restarts, retry on failure, or run exactly once across instances, you need something that lives outside the application. That is where the rest of this week is heading.

</aside>

Scheduling in production: trigger from outside

A common production pattern keeps the work inside your application, behind an HTTP endpoint, and lets an external scheduler trigger it. At work that scheduler is often a Kubernetes CronJob or a cloud scheduler — but you already know a free one: GitHub Actions (Week 12) can run a workflow on a schedule:

name: nightly-cleanup
on:
  schedule:
    - cron: "0 3 * * *" # 03:00 UTC every day

jobs:
  trigger:
    runs-on: ubuntu-latest
    steps:
      - name: Call the cleanup endpoint
        run: |
          curl --fail -X POST "${{ secrets.CLEANUP_URL }}" \
            -H "X-Job-Token: ${{ secrets.CLEANUP_TOKEN }}"

The endpoint must be secured: it should check the token before doing anything (Week 10 thinking — this endpoint deletes data). Note that GitHub's cron has five fields (no seconds) and runs in UTC.

<aside> ⚠️

GitHub's scheduled workflows are best-effort: runs can be delayed at busy times, and schedules on inactive public repositories are disabled after about 60 days. Fine for a cleanup job — not for business-critical timing.

</aside>

<aside> 💡

If a multi-instance application must keep @Scheduled inside the app, a distributed lock library like ShedLock (backed by the database you already have) makes sure only one instance runs each job. Awareness only — link below.

</aside>

💬 Your app sends a weekly newsletter with @Scheduled and runs as three instances in production. What happens, and what are two possible fixes?

Summary

@Async and @Scheduled move work off the request path with almost no setup, and they are fine for a single instance. But they do not survive restarts, do not retry, and duplicate across instances. Production systems therefore trigger periodic jobs from outside — and for background work that must never be lost, they use the tool coming up in the next chapters: the message queue.

Extra resources


The HackYourFuture curriculum is licensed under CC BY-NC-SA 4.0 *https://hackyourfuture.net/*

CC BY-NC-SA 4.0 Icons

Built with ❤️ by the HackYourFuture community · Thank you, contributors

Found a mistake or have a suggestion? Let us know in the feedback form.