Description / Meta Description
Learn how to build a generic enterprise-grade data synchronization framework using Spring Boot, Spring Batch, Quartz Scheduler, and H2 Database. This article provides a complete project skeleton including Maven dependencies, package structure, configuration classes, Quartz scheduling, batch processing, REST API triggers, unit testing strategy, and extensible architecture for future synchronization jobs.
Introduction
Most enterprise systems need some form of data synchronization.
Examples include:
- CRM → Data Warehouse
- Product Catalog → Search Platform
- ERP → Reporting Database
- Legacy Mainframe → Modern Application
- Third Party REST API → Internal Database
A common requirement looks like:
Source System
│
▼
REST API
│
▼
Spring Batch Job
│
▼
Target Database
Requirements typically include:
- Run every 24 hours
- Support manual triggering
- Handle failures gracefully
- Track execution history
- Support future jobs
- Restart failed executions
- Scale for large datasets
This is where Spring Batch and Quartz work extremely well together.
Solution Architecture
Quartz Scheduler
│
▼
Spring Batch Job Launcher
│
▼
Generic Batch Framework
│
┌─────────────────┼─────────────────┐
│ │ │
REST Reader Processor DB Writer
│ │ │
▼ ▼ ▼
Source API Transform Data H2/Oracle
Technology Stack
| Component | Purpose |
|---|---|
| Spring Boot | Application Framework |
| Spring Batch | Batch Processing |
| Quartz | Scheduling |
| Spring Data JPA | Persistence |
| H2 Database | Local Testing |
| Oracle | Production Database |
| RestTemplate/WebClient | Source API Calls |
| JUnit 5 | Unit Testing |
| Mockito | Mocking |
Maven Dependencies
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>
spring-boot-starter-web
</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>
spring-boot-starter-batch
</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>
spring-boot-starter-quartz
</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>
spring-boot-starter-data-jpa
</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc11</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>
spring-boot-starter-test
</artifactId>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
</dependency>
</dependencies>
Project Structure
com.example.datasync
├── config
│ ├── BatchConfig
│ ├── QuartzConfig
│ └── RestTemplateConfig
│
├── controller
│ └── JobController
│
├── batch
│ ├── reader
│ ├── processor
│ ├── writer
│ └── jobs
│
├── entity
│ └── ProductEntity
│
├── dto
│ └── ProductDTO
│
├── repository
│ └── ProductRepository
│
├── scheduler
│ └── ProductSyncQuartzJob
│
├── service
│ └── ProductSyncService
│
└── tests
Configuration
application.yml
spring:
datasource:
url: jdbc:h2:mem:testdb
driverClassName: org.h2.Driver
username: sa
password:
h2:
console:
enabled: true
batch:
jdbc:
initialize-schema: always
quartz:
job-store-type: jdbc
Oracle Profile
application-prod.yml
spring:
datasource:
url: jdbc:oracle:thin:@localhost:1521/XEPDB1
username: batchuser
password: password
driver-class-name:
oracle.jdbc.OracleDriver
Switching environments becomes:
--spring.profiles.active=prod
Sample Product Sync Flow
Assume source system exposes:
GET /products
Response:
[
{
"id":1,
"name":"Laptop"
}
]
Target:
PRODUCT_MASTER
table.
DTO
public class ProductDTO {
private Long id;
private String name;
}
Entity
@Entity
@Table(name="PRODUCT_MASTER")
public class ProductEntity {
@Id
private Long id;
private String name;
}
Repository
@Repository
public interface ProductRepository
extends JpaRepository<
ProductEntity,
Long> {
}
Reader
The reader fetches products from source API.
@Component
public class ProductApiReader {
public List<ProductDTO> fetch() {
RestTemplate restTemplate =
new RestTemplate();
return Arrays.asList(
restTemplate.getForObject(
"http://source/products",
ProductDTO[].class));
}
}
Processor
Transforms DTO into Entity.
@Component
public class ProductProcessor {
public ProductEntity process(
ProductDTO dto) {
ProductEntity entity =
new ProductEntity();
entity.setId(dto.getId());
entity.setName(dto.getName());
return entity;
}
}
Writer
Persists entities.
@Component
@RequiredArgsConstructor
public class ProductWriter {
private final ProductRepository
repository;
public void write(
List<ProductEntity> products) {
repository.saveAll(products);
}
}
Service Layer
@Service
@RequiredArgsConstructor
public class ProductSyncService {
private final ProductApiReader reader;
private final ProductProcessor processor;
private final ProductWriter writer;
public void syncProducts() {
List<ProductDTO> dtos =
reader.fetch();
List<ProductEntity> entities =
dtos.stream()
.map(processor::process)
.collect(Collectors.toList());
writer.write(entities);
}
}
Spring Batch Job
@Configuration
public class BatchConfig {
@Bean
public Job productSyncJob(
JobRepository repository,
Step productStep) {
return new JobBuilder(
"productSyncJob",
repository)
.start(productStep)
.build();
}
}
Step Configuration
@Bean
public Step productStep(
JobRepository repository,
PlatformTransactionManager tx) {
return new StepBuilder(
"productStep",
repository)
.tasklet((c, ctx) -> {
syncService.syncProducts();
return RepeatStatus.FINISHED;
}, tx)
.build();
}
Quartz Scheduler
Runs every 24 hours.
@Configuration
public class QuartzConfig {
@Bean
public Trigger trigger(
JobDetail detail) {
return TriggerBuilder
.newTrigger()
.withSchedule(
SimpleScheduleBuilder
.repeatHourlyForever(24)
)
.forJob(detail)
.build();
}
}
Quartz Job
public class ProductSyncQuartzJob
extends QuartzJobBean {
@Autowired
JobLauncher launcher;
@Autowired
Job productSyncJob;
@Override
protected void executeInternal(
JobExecutionContext ctx) {
launcher.run(
productSyncJob,
new JobParameters());
}
}
Manual Trigger REST API
@RestController
@RequestMapping("/jobs")
@RequiredArgsConstructor
public class JobController {
private final JobLauncher launcher;
private final Job productSyncJob;
@PostMapping("/product-sync")
public String runJob()
throws Exception {
launcher.run(
productSyncJob,
new JobParameters());
return "Job Triggered";
}
}
Spring Batch Metadata Tables
Automatically created.
BATCH_JOB_INSTANCE
BATCH_JOB_EXECUTION
BATCH_JOB_EXECUTION_PARAMS
BATCH_STEP_EXECUTION
BATCH_JOB_EXECUTION_CONTEXT
BATCH_STEP_EXECUTION_CONTEXT
These provide:
Execution History
Failure Tracking
Restartability
Audit Trail
Quartz Tables
When JDBC job store is enabled:
QRTZ_JOB_DETAILS
QRTZ_TRIGGERS
QRTZ_CRON_TRIGGERS
QRTZ_FIRED_TRIGGERS
QRTZ_LOCKS
Provide scheduling visibility.
Generic Job Configuration
Instead of hardcoding jobs:
Create:
CREATE TABLE SYNC_JOB_CONFIG (
JOB_NAME VARCHAR(100),
API_URL VARCHAR(500),
TARGET_TABLE VARCHAR(100),
ACTIVE_FLAG CHAR(1)
);
Example:
PRODUCT_SYNC
CUSTOMER_SYNC
ACCOUNT_SYNC
Future jobs become:
Configuration Changes
instead of:
Code Changes
Unit Test Example
ProductProcessorTest
@ExtendWith(MockitoExtension.class)
class ProductProcessorTest {
ProductProcessor processor =
new ProductProcessor();
@Test
void shouldConvertDtoToEntity() {
ProductDTO dto =
new ProductDTO();
dto.setId(1L);
dto.setName("Laptop");
ProductEntity entity =
processor.process(dto);
assertEquals(
dto.getId(),
entity.getId());
assertEquals(
dto.getName(),
entity.getName());
}
}
Service Layer Test
@ExtendWith(MockitoExtension.class)
class ProductSyncServiceTest {
@Mock
ProductApiReader reader;
@Mock
ProductWriter writer;
@InjectMocks
ProductSyncService service;
@Test
void shouldProcessProducts() {
when(reader.fetch())
.thenReturn(
Collections.emptyList());
service.syncProducts();
verify(reader).fetch();
}
}
Design Patterns Used
Template Method
Spring Batch Job Flow
Read
↓
Process
↓
Write
Strategy Pattern
Different Readers
RestReader
KafkaReader
FileReader
can be swapped.
Factory Pattern
Spring Bean Creation.
Command Pattern
Quartz triggers batch jobs.
Builder Pattern
JobBuilder
StepBuilder
Observer Pattern
Job listeners.
Singleton Pattern
Spring-managed beans.
Future Enhancements
Add:
Partitioning
Multi-threading
Retry Logic
Dead Letter Queue
Kafka Support
Cloud Storage Support
without changing architecture.
Final Thoughts
This starter project provides a simple but extensible foundation for building enterprise-grade data synchronization platforms using:
- Spring Boot
- Spring Batch
- Quartz
- H2 Database
while remaining fully compatible with Oracle, MySQL, PostgreSQL, or cloud-managed databases later.
The architecture deliberately separates:
Scheduling
Processing
Persistence
Configuration
allowing teams to onboard new synchronization jobs with minimal code changes while benefiting from Spring Batch’s built-in restartability, execution tracking, and operational visibility.