Week 1: Spring Boot Setup & @Scheduled Basics (Days 1–3)

Lesson 1 15 min

Introduction

Most “intro week” curricula leave you with a folder of disconnected demos: one project prints “hello” on a timer, another swaps fixedRate for cron, and a third tweaks thread pool sizes in isolation. That is fine for drills. It is weaker preparation for how engineers actually work—where scheduling semantics, concurrency, and observability show up in the same deployable unit, behind REST endpoints, under tests, and sometimes in front of an operator dashboard.

The week1taskschedulerintegratedproject tree exists to consolidate Week 1 (Days 1–3) of a code-first Java task-scheduling path into something you can run, extend, and reason about as a system. Day 1 lands as the first real scheduled surface: @EnableScheduling, a HelloSchedulerService with fixedRate, and status exposed over HTTP. Day 2 adds the three timing strategies that matter in production—fixedRate, fixedDelay, and cron—plus an in-memory task registry and execution history. Day 3 promotes ThreadPoolTaskScheduler to the execution engine so every @Scheduled method in the JVM shares one pool, with Micrometer gauges and a Thymeleaf dashboard that refreshes from aggregated APIs.

For engineers and serious learners, the payoff is not “I finished three pom.xml files.” It is: I can trace how a trigger becomes a runnable, how pool pressure shows up in metrics, and where each Spring scheduling construct earns its place before persistence, dynamic registration, and distributed workers enter the story (Days 7–60 in taskscheduler-p).


From Fundamentals to a Unified System

The progression is intentionally layered rather than repetitive.

Day 1 (day1/hello-task-scheduler) establishes the outer contract: Spring Boot starts, scheduling is enabled on IntegratedTaskSchedulerApplication, and HelloSchedulerService fires on a configurable interval (hello.scheduler.interval-ms, default 10 seconds). Execution count and elapsed time are observable via GET /api/hello/status. In the standalone Day 1 app, a 60-second demo could call System.exit(0); the integrated build runs continuously so the hello job coexists with Days 2–3.

Day 2 (day2/task-scheduler-day2) moves timing semantics into first-class scheduled methods. TaskSchedulerService runs:

  • fixedRate health checks (every 5s from trigger start),

  • fixedDelay cleanup (15s after the previous run completes),

  • cron report generation (demo: every minute via 0 * * * * ?).

Each run is recorded as a TaskExecution in a bounded ConcurrentLinkedQueue (last 50 entries). TaskService holds logical task metadata—name, type, schedule expression, ACTIVE / PAUSED / ERROR—with CRUD at /api/tasks. Scheduling stops being “one annotation” and becomes a small operational domain.

Day 3 (day3/thread-pool-scheduler) replaces the default single-thread scheduler with an explicit ThreadPoolTaskScheduler bean (SchedulerConfig), wired globally through IntegratedSchedulingConfigurer (SchedulingConfigurer). ScheduledTasksService runs six synthetic workloads at different cadences to exercise concurrency. MonitoringService registers Micrometer gauges (active threads, pool size, queue depth); Actuator exposes Prometheus at /actuator/prometheus.

Nothing here replaces Quartz, Kubernetes CronJobs, or a distributed coordinator. Everything here rehearses the shapes production schedulers inherit: declarative triggers, non-overlapping cleanup semantics, shared executors, and metrics you can scrape.


Architecture Overview

At a high level, the system is a single JVM with two client surfaces sharing one thread pool:

Operator path — Browser → Thymeleaf dashboard (/, tabs for Overview / Day 1 / Day 2 / Day 3) → integrated-dashboard.js polls /api/dashboard-data every 3 seconds.

API path — HTTP clients or tests → REST controllers (HelloController, TaskController, IntegratedDashboardController) → service layer → in-memory state and scheduled method bodies.

Execution path — Spring’s scheduling infrastructure → ThreadPoolTaskScheduler (custom-scheduler-* threads) → all @Scheduled methods (Day 1 hello, Day 2 health/cleanup/report, Day 3 pool stress tasks).

Observability path — Actuator (/actuator/health, /actuator/scheduledtasks, /actuator/prometheus) + custom gauges from MonitoringService.

Standalone Day 1–3 folders under taskscheduler-p/dayN/ remain valid reference implementations; the integrated tree is the capstone that composes them without duplicating three separate processes on port 8080.

Architecture diagram

Component Architecture

Week 1 Integrated Task Scheduler Architecture Browser / Client Dashboard + REST Spring Boot Application (port 8080) Web Layer IntegratedDashboardController � HelloController � TaskController Day 1 HelloSchedulerService fixedRate hello /api/hello/status Day 2 TaskSchedulerService fixedRate � fixedDelay cron + TaskService Day 3 ScheduledTasksService pool workload tasks MonitoringService ThreadPoolTaskScheduler (custom-scheduler-*) SchedulerConfig + IntegratedSchedulingConfigurer pool-size: 10 � max-pool-size: 20 � CallerRunsPolicy Actuator / Prometheus All @Scheduled methods share one thread pool � In-memory task registry � No external DB (Week 1)

Data / Control Flow

Two flows deserve separate mental models—scheduled execution and HTTP observation—but they rhyme.

Scheduled execution: Trigger (rate / delay / cron) → Spring registers task on ScheduledTaskRegistrarIntegratedSchedulingConfigurer routes work to customTaskScheduler → worker thread runs @Scheduled method → service updates counters, history queue, or TaskExecutionMetrics → logs to stdout/SLF4J.

HTTP observation: GET /api/dashboard-dataIntegratedDashboardController aggregates hello stats, Day 2 execution history + task registry, Day 3 pool stats + per-task metrics → JSON → dashboard JS updates DOM and Chart.js series. Handlers are read-mostly and do not enqueue work; they snapshot process memory.

Day 2 write path: POST /api/tasksTaskControllerTaskService.createTask → mutates ConcurrentHashMap registry → response returns enriched Task (id, nextRun, status). Pause/resume flip TaskStatus without stopping the underlying @Scheduled beans in this Week 1 build—an intentional teaching gap called out in the state-machine section.

Control-flow diagram

Flowchart

Integrated Scheduler Execution Flow Application Start @EnableScheduling + Create ThreadPoolTaskScheduler Register @Scheduled methods Hello � Health/Cleanup/Report � Pool tasks (6) Trigger fires? rate / delay / cron Submit to ThreadPoolTaskScheduler Assign custom-scheduler-N thread Execute scheduled method body Success? Yes Record SUCCESS history / metrics No Record ERROR log exception Update Micrometer gauges + dashboard APIs /api/dashboard-data refreshes UI Wait for next trigger HTTP requests read snapshots; they do not drive scheduling

###Flow-Chart Diagram

Figure 2. Application startup registers all @Scheduled methods on the shared pool; each trigger cycle runs execute → record → metrics; HTTP only reads snapshots.

Nuance: In a long-running Spring Boot process, “state update” for executions is append-only history in TaskSchedulerService and atomic counters in ScheduledTasksService. The Day 2 task registry is mutable CRUD state; scheduled beans themselves are static at compile time until later course days introduce dynamic registration (Day 9).


State Management

Week 1 persistence is in-memory only—no JDBC, no Redis. Three state shapes coexist:

Hello layer (Day 1)HelloSchedulerService keeps executionCount and startTime (initialized at field declaration and reinforced on ApplicationReadyEvent). No shutdown timer in the integrated app.

Registry + history (Day 2)TaskService stores Task entities in a ConcurrentHashMap (default seeds for health, cleanup, report). TaskSchedulerService stores TaskExecution records in a ConcurrentLinkedQueue capped at 50. Status fields (ACTIVE, PAUSED, ERROR) describe operator intent; underlying scheduled methods still run unless you extend the design.

Pool metrics (Day 3)TaskExecutionMetrics per logical task name (quickTask, mediumTask, …) with AtomicLong counts and volatile last-run metadata. MonitoringService reads ThreadPoolExecutor statistics from the shared scheduler bean.

The HTTP surface for Day 7-style file listing does not exist here; Week 1 stops at RAM. Later taskscheduler-p days add Flyway schemas, JPA entities, and API-backed definitions.

State machine (Day 2 task registry)

State Machine

Task Lifecycle State Machine (Day 2 Registry) Logical tasks in TaskService metadata mirrors scheduled jobs CREATED POST /api/tasks ACTIVE Scheduler running PAUSED POST .../pause ERROR Run failed DELETED DELETE /api/tasks start activate pause resume execution fails recover / retry success delete delete from ACTIVE create as paused Runtime execution (all states) SCHEDULED � RUNNING � COMPLETED SCHED RUN DONE Recorded in TaskSchedulerService history PAUSED stops registry updates; underlying @Scheduled beans continue unless disabled in a future iteration

###State machine Diagram

Figure 3. Logical tasks transition CREATED → ACTIVE ↔ PAUSED → ERROR → DELETED via REST; runtime runs move SCHEDULED → RUNNING → COMPLETED in execution history.

Reading the diagram: POST /api/tasks/{id}/pause sets PAUSED on metadata; resume recalculates nextRun. Failed scheduled runs set execution rows to ERROR without automatically flipping registry status—mirroring how ops teams often separate “job failed once” from “definition disabled.”


Key Engineering Insights

Lesson boundaries vs. integration — Days 1–3 remain separate packages and concepts on purpose. Unifying the system does not mean one mega-class with every @Scheduled method—it means one executor, consistent REST contracts, and a dashboard that proves they coexist under load.

fixedRate vs. fixedDelay — Health checks use fixedRate (steady cadence). Cleanup uses fixedDelay (avoid overlap when work runs long). Confusing the two is a common production bug; Week 1 makes both visible in logs and execution history.

Single pool, many jobs — After Day 3, Day 1’s hello task and Day 2’s slow cleanup share threads with Day 3’s highFrequencyTask. Watch queueSize and activeThreads when tuning task.scheduler.pool-size.

Stateless HTTP vs. stateful scheduler — REST handlers return snapshots; the scheduler holds the truth in running threads and in-memory structures. Later days externalize definitions to a database without changing the mental model of “trigger → executor → record.”

Observability in miniature/actuator/scheduledtasks lists registered schedules; Prometheus gauges rehearse the habit: if you cannot see pool saturation, you cannot right-size workers.

Extensibility hook — Replacing simulated Thread.sleep work with real I/O should touch service methods first, not IntegratedDashboardController response shapes—pressure that keeps HTTP stable as implementations mature.


Success Metrics

Modularity

  • Days 1–3 logic lives in distinct service classes (hello, TaskSchedulerService, ScheduledTasksService).

  • Configuration is isolated in SchedulerConfig + IntegratedSchedulingConfigurer.

  • Controllers stay thin; no scheduling annotations on web layer classes.

Readability

  • Dashboard tabs map 1:1 to curriculum days.

  • application.yml documents hello interval and pool sizing in one place.

  • Execution history table shows task name, type, status, duration without spelunking logs.

Reusability

  • Day 2 Task / TaskExecution models are reusable when JPA entities arrive (Day 7+).

  • MonitoringService.ThreadPoolStats record is a stable API for UI and tests.

  • Standalone dayN/ projects remain runnable for diffing against the integrated build.

Execution Correctness

  • mvn test passes (IntegratedTaskSchedulerApplicationTests loads context, asserts pool size 5 under test properties).

  • Manual validation includes:

    • Hello execution count increasing on /api/hello/status

      • Health/cleanup/report counters moving on /api/dashboard-data

        • Thread pool charts updating on Day 3 tab

          • GET /api/tasks returning seeded registry

            • Actuator health UP

Extensibility

  • Adding a new scheduled job requires: a method on a @Service with @Scheduled, optional metrics registration, and a dashboard row if it should be visible.

  • Adding a new REST endpoint requires: controller method + service call; no change to thread pool config unless load profile shifts.

  • Adding a new registry task requires: POST /api/tasks body with TaskType + schedule string; no schema migration in Week 1.


Conclusion

week1taskschedulerintegratedproject is not a toy checklist of annotations—it is a deliberately small multi-layer system: declarative schedules, three timing strategies, a shared ThreadPoolTaskScheduler, REST observability, and a browser dashboard each exercising a different seam of the same runtime. You still get the discrete wins of a three-day crash course, but you also get the engineer’s reward: tracing how triggers, executors, history, and metrics cooperate when the console is not the only client.

Carry that forward into the rest of taskscheduler-p: persistence and Flyway migrations (Day 7), definition APIs (Day 8), dynamic schedulers (Day 9), and eventually distributed workers (Day 60) all assume you already understand @EnableScheduling, when to use delay versus rate, and why pool metrics matter. If Week 1 already trains you to see those layers at once, the heavier days ahead slot into scaffolding you know how to build.


Appendix: Repository layout

Code
week_1_taskscheduler_integrated_project/
├── task-scheduler-integrated/     # Runnable Spring Boot app
│   ├── src/main/java/com/taskscheduler/
│   │   ├── IntegratedTaskSchedulerApplication.java
│   │   ├── config/                # SchedulerConfig, IntegratedSchedulingConfigurer
│   │   ├── hello/                 # Day 1
│   │   ├── service/               # Day 2 + Day 3
│   │   ├── controller/
│   │   └── model/
│   ├── src/main/resources/
│   │   ├── application.yml
│   │   ├── templates/dashboard.html
│   │   └── static/
│   ├── start.sh · stop.sh
│   └── pom.xml
├── article/                       # This document
└── diagrams/
    ├── architecture.svg
    ├── flowchart.svg
    └── state-machine.svg
Need help?