Imported from xman-berlin/spring-multiple-datasources (
AGENTS.md). Install upstream withnpx skills add xman-berlin/spring-multiple-datasources. Copyright stays with the author.
AGENTS.md - Spring Multiple Data Sources Project
This file provides essential information for agentic coding assistants working on this Spring Boot Java project.
Project Overview
- Framework: Spring Boot 3.5.10 with Spring Data JPA
- Java Version: 17
- Build Tool: Maven
- Package:
at.geise.test.springmultipledatasources - Database: H2 (development/test), PostgreSQL (production)
- Key Dependencies: Web, JPA, Actuator, Validation, Lombok, TestContainers
Build and Development Commands
Maven Commands (with Maven wrapper support)
# Project compilation and validation
mvn clean compile # Clean and compile all classes
mvn compile # Incremental compile
mvn clean install # Clean, compile, test, and install to local repo
mvn clean package # Clean, compile, test, and package (creates JAR)
mvn clean package -DskipTests # Build JAR without running tests
# Testing (single test focus)
mvn test # Run all tests
mvn test -Dtest=ClassName # Run specific test class
mvn test -Dtest=ClassName#methodName # Run specific test method
mvn test -Dtest="*Integration*" # Run tests matching pattern
mvn clean verify # Run tests and verify (includes integration tests)
# Application execution
mvn spring-boot:run # Run Spring Boot application
mvn spring-boot:run -Dspring-boot.run.profiles=test # Run with specific profile
mvn spring-boot:run -Dspring-boot.run.jvmArguments="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005" # Debug mode
# Dependency and project analysis
mvn dependency:tree # Show dependency tree
mvn versions:display-dependency-updates # Check for dependency updates
mvn versions:display-plugin-updates # Check for plugin updates
mvn site # Generate project documentation site
# Docker integration (requires docker/docker-compose.yml)
docker-compose -f docker/docker-compose.yml up -d # Start PostgreSQL containers
docker-compose -f docker/docker-compose.yml down # Stop containers
# Quick health check after startup
curl http://localhost:8080/actuator/health # Basic health endpoint
curl http://localhost:8080/api/health # Application-specific health
Development Workflow
- Setup:
mvn clean compileto verify compilation - Test iteratively: Use single test runs (
mvn test -Dtest=ClassName) during development - Full verification:
mvn clean installbefore commits - Debug:
mvn spring-boot:runwith debug JVM arguments when troubleshooting
Code Style Guidelines
Import Organization
- Order alphabetically within each group
- Group imports in this order:
- Project-specific imports (
at.geise.test.*) - Third-party libraries (Lombok, HikariCP)
- Spring framework imports
- JEE/Jakarta specifications (JPA, Transaction, Validation)
- Java standard library imports
- Project-specific imports (
// Example import order
import at.geise.test.springmultipledatasources.model.primary.PrimaryEntity;
import at.geise.test.springmultipledatasources.repository.primary.PrimaryRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import jakarta.persistence.EntityManager;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import java.util.Optional;
Naming Conventions
- Classes: PascalCase (e.g.,
PrimaryService,DataSourceConfig,ApiExceptionHandler) - Interfaces: PascalCase ending with descriptive suffix (e.g.,
PrimaryRepository,UserService) - Methods: camelCase starting with verb (e.g.,
findById,saveEntity,getAllEntities,processRequest) - Variables: camelCase (e.g.,
primaryRepository,dataSource,entityList) - Constants: UPPER_SNAKE_CASE (e.g.,
MAX_POOL_SIZE,DEFAULT_TIMEOUT) - Packages: lowercase with dots (e.g.,
repository.primary,controller.api) - URLs and API paths: kebab-case (e.g.,
/api/primary/entities,/api/secondary/status)
Code Structure and Patterns
Entity Patterns
@Entity
@Table(name = "primary_entities")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PrimaryEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name", nullable = false, length = 255)
@NotBlank(message = "Name is required")
private String name;
@Column(name = "description", columnDefinition = "TEXT")
private String description;
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}
Service Patterns
@Service
@RequiredArgsConstructor
@Slf4j
@Transactional(transactionManager = "primaryTransactionManager")
public class PrimaryService {
private final PrimaryRepository primaryRepository;
private final EntityManager entityManager;
@Transactional(readOnly = true)
public List<PrimaryEntity> getAllEntities() {
log.debug("Fetching all primary entities");
return primaryRepository.findAll();
}
public PrimaryEntity saveEntity(PrimaryEntity entity) {
log.debug("Saving entity with name: {}", entity.getName());
validateEntity(entity);
return primaryRepository.save(entity);
}
private void validateEntity(PrimaryEntity entity) {
if (entity.getName() == null || entity.getName().trim().isEmpty()) {
throw new IllegalArgumentException("Entity name cannot be null or empty");
}
}
}
Controller Patterns
@RestController
@RequestMapping("/api/primary")
@RequiredArgsConstructor
@Slf4j
@Validated
public class PrimaryController {
private final PrimaryService primaryService;
@GetMapping("/entities")
public ResponseEntity<List<PrimaryEntityDto>> getAllEntities() {
log.info("GET /api/primary/entities - Fetching all entities");
List<PrimaryEntity> entities = primaryService.getAllEntities();
List<PrimaryEntityDto> dtos = entities.stream()
.map(this::convertToDto)
.collect(Collectors.toList());
return ResponseEntity.ok(dtos);
}
@PostMapping("/entities")
public ResponseEntity<PrimaryEntityDto> createEntity(
@Valid @RequestBody CreateEntityRequest request) {
log.info("POST /api/primary/entities - Creating entity: {}", request.getName());
PrimaryEntity entity = convertToEntity(request);
PrimaryEntity saved = primaryService.saveEntity(entity);
return ResponseEntity.created(buildLocationUri(saved.getId()))
.body(convertToDto(saved));
}
private PrimaryEntityDto convertToDto(PrimaryEntity entity) {
return PrimaryEntityDto.builder()
.id(entity.getId())
.name(entity.getName())
.description(entity.getDescription())
.createdAt(entity.getCreatedAt())
.build();
}
private URI buildLocationUri(Long id) {
return ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(id)
.toUri();
}
}
Repository Patterns
@Repository
public interface PrimaryRepository extends JpaRepository<PrimaryEntity, Long> {
List<PrimaryEntity> findByNameContainingIgnoreCase(String name);
Optional<PrimaryEntity> findByName(String name);
boolean existsByName(String name);
@Query("SELECT e FROM PrimaryEntity e WHERE e.createdAt >= :since")
List<PrimaryEntity> findRecentEntities(@Param("since") LocalDateTime since);
@Modifying
@Query("UPDATE PrimaryEntity e SET e.updatedAt = :now WHERE e.id = :id")
int updateTimestamp(@Param("id") Long id, @Param("now") LocalDateTime now);
}
Error Handling and Validation
Global Exception Handler
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<ErrorResponse> handleEntityNotFound(EntityNotFoundException ex) {
log.warn("Entity not found: {}", ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("NOT_FOUND", ex.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationErrors(MethodArgumentNotValidException ex) {
log.warn("Validation error: {}", ex.getMessage());
Map<String, String> errors = ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(
FieldError::getField,
error -> Optional.ofNullable(error.getDefaultMessage()).orElse("Invalid value")
));
return ResponseEntity.badRequest()
.body(new ErrorResponse("VALIDATION_ERROR", "Validation failed", errors));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
log.error("Unexpected error", ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse("INTERNAL_ERROR", "An unexpected error occurred"));
}
}
DTO and Request Patterns
// DTO for responses
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PrimaryEntityDto {
private Long id;
private String name;
private String description;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime createdAt;
}
// Request object for creation
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateEntityRequest {
@NotBlank(message = "Name is required")
@Size(min = 2, max = 255, message = "Name must be between 2 and 255 characters")
private String name;
@Size(max = 1000, message = "Description cannot exceed 1000 characters")
private String description;
}
DataSource Configuration Pattern
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
basePackages = "at.geise.test.springmultipledatasources.repository.primary",
entityManagerFactoryRef = "primaryEntityManagerFactory",
transactionManagerRef = "primaryTransactionManager"
)
public class PrimaryDataSourceConfig {
@Bean
@Primary
@ConfigurationProperties("spring.datasource.primary")
public DataSourceProperties primaryDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@Primary
@ConfigurationProperties("spring.datasource.primary.hikari")
public DataSource primaryDataSource() {
return primaryDataSourceProperties()
.initializeDataSourceBuilder()
.type(HikariDataSource.class)
.build();
}
@Bean(name = "primaryEntityManagerFactory")
@Primary
public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(
EntityManagerFactoryBuilder builder,
@Qualifier("primaryDataSource") DataSource dataSource) {
return builder
.dataSource(dataSource)
.packages("at.geise.test.springmultipledatasources.model.primary")
.persistenceUnit("primary")
.properties(Map.of(
"hibernate.show_sql", "true",
"hibernate.format_sql", "true",
"hibernate.hbm2ddl.auto", "create-drop"
))
.build();
}
@Bean(name = "primaryTransactionManager")
@Primary
public PlatformTransactionManager primaryTransactionManager(
@Qualifier("primaryEntityManagerFactory") LocalContainerEntityManagerFactoryBean factory) {
return new JpaTransactionManager(factory.getObject());
}
}
Project Structure
src/main/java/at/geise/test/springmultipledatasources/
├── SpringMultipleDatasourcesApplication.java
├── config/
│ ├── PrimaryDataSourceConfig.java
│ ├── SecondaryDataSourceConfig.java
│ ├── DataInitializer.java
│ └── ConnectionMonitor.java
├── controller/
│ └── DataSourceController.java
├── model/
│ ├── primary/
│ │ └── PrimaryEntity.java
│ └── secondary/
│ └── SecondaryEntity.java
├── repository/
│ ├── primary/
│ │ └── PrimaryRepository.java
│ └── secondary/
│ └── SecondaryRepository.java
├── service/
│ ├── PrimaryService.java
│ └── SecondaryService.java
└── dto/
└── (Data Transfer Objects)
src/test/java/.../
├── SpringMultipleDatasourcesApplicationTests.java
└── (Additional test classes)
docker/
└── docker-compose.yml
Database Configuration
This project uses multiple data sources with strict separation:
- Primary: H2 in-memory (development/test), PostgreSQL (production)
- Secondary: H2 in-memory (on-demand only), separate connection pool
- Transaction management: Separate managers for each data source (
primaryTransactionManager,secondaryTransactionManager) - Entity separation: Strict package separation between data sources
- Lazy initialization: Secondary datasource only connects when needed
// Always specify transaction manager explicitly
@Service
@Transactional(transactionManager = "primaryTransactionManager")
public class PrimaryService { /* ... */ }
@Service
@Transactional(transactionManager = "secondaryTransactionManager")
public class SecondaryService { /* ... */ }
Testing Framework & Guidelines
Testing Stack
- Unit Testing: JUnit 5 + Mockito (+ AssertJ extensions)
- Integration Testing: TestContainers + H2/PostgreSQL
- Spring Testing:
@SpringBootTest,@DataJpaTest,@WebMvcTest - Coverage: JaCoCo (Maven plugin configured)
Test Patterns
@SpringBootTest
@ActiveProfiles("test")
class PrimaryServiceIntegrationTest {
@Autowired
private PrimaryService primaryService;
@Autowired
private PrimaryRepository primaryRepository;
@Test
@DisplayName("Should save and retrieve entity successfully")
void shouldSaveAndRetrieveEntity() {
// Given
PrimaryEntity entity = createTestEntity();
primaryRepository.save(entity);
// When
List<PrimaryEntity> entities = primaryService.getAllEntities();
// Then
assertThat(entities).isNotEmpty();
assertThat(entities.get(0).getName()).isEqualTo(entity.getName());
}
@Test
@DisplayName("Should throw exception for null entity")
void shouldThrowExceptionForNullEntity() {
assertThatThrownBy(() -> primaryService.saveEntity(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Entity cannot be null");
}
private PrimaryEntity createTestEntity() {
return PrimaryEntity.builder()
.name("Test Entity")
.description("Test Description")
.build();
}
}
Repository Testing
@DataJpaTest
@ActiveProfiles("test")
class PrimaryRepositoryTest {
@Autowired
private PrimaryRepository repository;
@Test
void shouldFindByName() {
// Given
PrimaryEntity entity = PrimaryEntity.builder()
.name("Test Entity")
.build();
entity = repository.save(entity);
// When
Optional<PrimaryEntity> found = repository.findByName("Test Entity");
// Then
assertThat(found).isPresent();
assertThat(found.get().getId()).isEqualTo(entity.getId());
}
}
Best Practices
Code Quality
- Always write tests for new functionality (unit + integration)
- Use constructor injection exclusively (no field injection)
- Specify transaction managers explicitly in
@Transactional - Validate all inputs using Bean Validation (
@Valid,@NotNull, etc.) - Use meaningful logging (DEBUG for operations, WARN for issues, ERROR for failures)
- Handle exceptions appropriately (custom exceptions, global handler)
Database & Transactions
- Separate entities strictly by data source packages
- Never cross data source boundaries without explicit handling
- Use DTOs for REST controllers (never expose entities directly)
- Specify read-only transactions for query operations (
@Transactional(readOnly = true)) - Use appropriate query methods and custom
@Queryannotations when needed
Multi-tenancy & Configuration
- Externalize configuration through
application.ymlproperties - Use profiles for different environments (
@Profile("test"),@Profile("prod")) - Qualifier beans explicitly when multiple beans of same type exist
- Configure connection pools appropriately for each datasource's usage pattern
Key Dependencies & Their Usage
- Spring Boot Starter Web: REST controllers, request mapping, validation
- Spring Data JPA: Repository pattern, custom queries, entity management
- Spring Boot Actuator: Health checks, metrics, monitoring endpoints
- HikariCP: High-performance JDBC connection pooling
- Lombok: Boilerplate reduction (
@Data,@Builder,@RequiredArgsConstructor) - TestContainers: Integration tests with real databases
- PostgreSQL/H2 Drivers: Database connectivity
Common Development Tasks
Adding New Entity
- Create entity class in appropriate model package (primary/secondary)
- Create repository interface extending
JpaRepository<Entity, ID> - Add service methods with proper transaction management
- Create DTOs for API responses (don't expose entities)
- Add REST endpoints in controller
- Write comprehensive tests
Adding New DataSource
- Create
XXXDataSourceConfig.javawith proper@Configuration - Configure Hikari properties in
application.yml - Create entities, repositories, and services in separate packages
- Add controller endpoints with proper routing
- Update database initialization if needed
- Test thoroughly with integration tests
IDE Configuration
- Java 17 SDK required
- Enable annotation processing for Lombok support
- UTF-8 encoding for all source files
- Spring Boot plugin recommended for enhanced support
- Configure code style: Follow IntelliJ Java conventions or Google Java Style
- Enable save actions: Optimize imports, reformat code on save
Troubleshooting
Build Issues
- Missing dependencies:
mvn dependency:resolve - Lombok compilation errors: Ensure annotation processing is enabled
- Java version conflicts: Verify JDK 17 in project and IDE settings
Runtime Issues
- Datasource connection failures: Check database URLs and credentials in
application.yml - Transaction errors: Verify correct transaction manager names
- Validation failures: Check Bean Validation annotations and messages
Testing Issues
- TestContainers not starting: Check Docker daemon and network connectivity
- Profile configuration: Use
@ActiveProfiles("test")for test-specific configs - Mock setup: Use
@ExtendWith(MockitoExtension.class)for unit tests
This guide should be updated when introducing new patterns, dependencies, or significant architectural changes.
