Building a Generic Data Synchronization Platform Using Spring Batch, Quartz, H2 and REST APIs

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

ComponentPurpose
Spring BootApplication Framework
Spring BatchBatch Processing
QuartzScheduling
Spring Data JPAPersistence
H2 DatabaseLocal Testing
OracleProduction Database
RestTemplate/WebClientSource API Calls
JUnit 5Unit Testing
MockitoMocking

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.

Leave a Reply

Your email address will not be published. Required fields are marked *