Part 5: Background Workers¶
Add a scheduled background worker using Spring Task and Redis for asynchronous processing alongside your web services.
What you'll learn¶
- Running a portless worker service in AppMecca
- Using Spring's
@Scheduledtasks with conditional activation - Enabling AppMecca's managed Redis cache
- Managing worker lifecycle with service commands
What you'll need¶
- A full-stack deployment from Part 4
Add Redis dependencies¶
// backend/build.gradle (add to existing dependencies)
dependencies {
// ... existing deps
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}
Create a task publisher¶
Define a TodoTask record and a publisher that pushes tasks to a Redis list:
// backend/src/main/java/com/example/todo/worker/TaskPublisher.java
package com.example.todo.worker;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
@Component
public class TaskPublisher {
private final StringRedisTemplate redis;
private final ObjectMapper mapper;
public TaskPublisher(StringRedisTemplate redis, ObjectMapper mapper) {
this.redis = redis;
this.mapper = mapper;
}
public void publish(TodoTask task) {
try {
String json = mapper.writeValueAsString(task);
redis.opsForList().rightPush("todo:tasks", json);
} catch (Exception e) {
throw new RuntimeException("Failed to publish task", e);
}
}
}
Create the scheduled processor¶
This component only activates when the worker.enabled property is true.
// backend/src/main/java/com/example/todo/worker/TaskProcessor.java
package com.example.todo.worker;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
@ConditionalOnProperty(name = "worker.enabled", havingValue = "true")
public class TaskProcessor {
private static final Logger log = LoggerFactory.getLogger(TaskProcessor.class);
private final StringRedisTemplate redis;
private final ObjectMapper mapper;
public TaskProcessor(StringRedisTemplate redis, ObjectMapper mapper) {
this.redis = redis;
this.mapper = mapper;
}
@Scheduled(fixedDelay = 1000)
public void processTasks() {
String json = redis.opsForList().leftPop("todo:tasks");
if (json == null) return;
try {
TodoTask task = mapper.readValue(json, TodoTask.class);
log.info("Processing task: type={}, todoId={}", task.type(), task.todoId());
switch (task.type()) {
case "notify" -> log.info("Sending notification for todo {}", task.todoId());
case "audit" -> log.info("Auditing change for todo {}", task.todoId());
default -> log.warn("Unknown task type: {}", task.type());
}
} catch (Exception e) {
log.error("Failed to process task: {}", e.getMessage());
}
}
}
ConditionalOnProperty
The @ConditionalOnProperty annotation ensures the scheduled task processor only starts when worker.enabled=true. The backend service runs without it, and the worker container sets it via an environment variable.
Enable scheduling¶
// backend/src/main/java/com/example/todo/TodoApplication.java
package com.example.todo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class TodoApplication {
public static void main(String[] args) {
SpringApplication.run(TodoApplication.class, args);
}
}
Configure Redis connection¶
# backend/src/main/resources/application.properties (add to existing)
spring.data.redis.host=${REDIS_HOST:localhost}
spring.data.redis.port=${REDIS_PORT:6379}
worker.enabled=${WORKER_ENABLED:false}
REDIS_HOST injection
AppMecca injects REDIS_HOST and REDIS_PORT automatically when cache: enabled: true is set in appmecca.yaml.
Update appmecca.yaml¶
Add the worker service and enable Redis:
database:
enabled: true
redis:
enabled: true
containers:
- name: backend
dockerfile: backend/Dockerfile
context: backend/
- name: frontend
dockerfile: frontend/Dockerfile
context: frontend/
deploy:
- name: backend
container: backend
port: 8080
path: /api
health_check:
path: /health
- name: frontend
container: frontend
type: spa
command: 'nginx -g "daemon off;"' # serves the built bundle from a container
spa_root: /usr/share/nginx/html
port: 3000
health_check:
path: /
environment:
PUBLIC_API_URL: "${service:backend}"
- name: worker
container: backend
command: "java -jar app.jar --worker.enabled=true"
resources:
memory: 512m # JVM floor — unit suffix is mandatory
Portless services
The worker service has no port field. AppMecca deploys it with no public hostname, no route on the platform HTTPS router, and no health check. It runs as a background process that communicates through Redis and the database.
Top-level cache: is the managed Redis
The cache: enabled: true block at the top level of appmecca.yaml turns on the managed Redis cache and injects REDIS_URL into every container. It defaults to off.
Do not confuse it with the per-service deploy[].cache: block, which configures edge caching for a web service and is an entirely different feature.
JVM services need explicit memory
A command: that starts with java or names a .jar triggers a validation warning if resources.memory is below 512m — JVM runtimes routinely get killed during startup at the platform's 128 MB default. The unit suffix is required: a bare 512 means 512 bytes, and 512mb is not a recognised suffix.
Publish tasks from the controller¶
Update TodoController to push tasks when todos are created:
@PostMapping("/api/todos")
public Todo create(@RequestBody Todo todo) {
Todo saved = repository.save(todo);
taskPublisher.publish(new TodoTask("notify", saved.getId(), saved.getTitle()));
return saved;
}
Build and deploy¶
Monitor the worker¶
Read the worker's container output to see tasks being processed. The service is selected with --service:
mecca logs todo-app --type container --service worker
mecca logs todo-app --type container --service worker --since 5m
Create a todo through the frontend and watch the worker log pick it up.
Worker lifecycle commands¶
These take the app name first and the service name second, both as positional arguments:
mecca app restart todo-app worker --force # Restart after config change
mecca app stop todo-app worker --force # Stop temporarily
mecca app start todo-app worker # Start again
Stopping vs undeploying
mecca app stop halts a single service. mecca app undeploy removes the whole deployment. Use app stop when you want to pause the worker without affecting the frontend and backend.
Command reference¶
| Command | Purpose |
|---|---|
mecca app restart <app> <service> --force |
Restart a service |
mecca app stop <app> <service> --force |
Stop a service |
mecca app start <app> <service> |
Start a stopped service |
mecca logs <app> --type container --service <svc> |
Read a service's output |
What's next¶
In Part 6: Branch Deployments, you will deploy a feature branch side-by-side with your main deployment to test changes before merging.