Enterprise Features
1. Overview
ElastiCORE goes beyond simple CRUD automation to support a wide range of features required in large-scale enterprise environments. From multi-datasource separation and per-domain blueprint file management to custom interface implementations, audit logging, security integration, and caching strategies — ElastiCORE provides the tools needed to organize complex systems in a structured and maintainable way.
This document is especially useful for medium-to-large projects where multiple teams collaborate or where multiple databases and domains are involved.
2. Multiple DataSource Configuration
Defining Datasources in env.yml
Large-scale services often separate databases by business domain. ElastiCORE allows you to declare multiple datasources in env.yml and explicitly specify which datasource each Port should use.
# src/main/resources/elasticore/env.yml
datasource:
main:
type: jpa
url: jdbc:postgresql://db-main:5432/maindb
username: app_user
password: ${DB_MAIN_PASSWORD}
hikari:
maximum-pool-size: 20
analytics:
type: jpa
url: jdbc:postgresql://db-analytics:5432/analyticsdb
username: analytics_user
password: ${DB_ANALYTICS_PASSWORD}
hikari:
maximum-pool-size: 10
log:
type: jpa
url: jdbc:mysql://db-log:3306/logdb
username: log_user
password: ${DB_LOG_PASSWORD}
In production environments, it is strongly recommended to inject passwords using the ${ENV_VAR} syntax. Integrating with Kubernetes Secrets or AWS Secrets Manager can further improve security.
Specifying a Datasource in Port DSL
When defining a port, use the @datasource("...") annotation to specify which datasource it should use.
transaction:
port:
# User service using the main DB
UserService:
meta: dbms @datasource("main")
methods:
findByEmail:
params:
email: string
return: User
# Analytics service using the analytics DB
AnalyticsService:
meta: dbms @datasource("analytics")
methods:
getDailyStats:
params:
date: localdate
return: DailyStatsDTO
# Audit service accessing the log DB
AuditLogService:
meta: dbms @datasource("log")
methods:
saveAuditLog:
params:
log: AuditLogDTO
return: void
Datasource Separation Example per Transaction Port
The code generated by ElastiCORE configures independent EntityManagerFactory instances and transaction managers for each datasource. Distributed transactions that cross domain boundaries must therefore be handled explicitly.
// Example of generated service code (for reference)
@Service
@Transactional("mainTransactionManager")
public class UserServiceImpl implements UserService {
// Operates within the main datasource transaction scope
}
@Service
@Transactional("analyticsTransactionManager")
public class AnalyticsServiceImpl implements AnalyticsService {
// Operates within the analytics datasource transaction scope
}
If you need a single transaction spanning multiple datasources, consider applying JTA (Java Transaction API) or the Saga pattern. ElastiCORE automatically manages single-datasource transactions, but distributed transactions must be handled at the application level.
3. Large-Scale Model Management
Strategy for Splitting Blueprint Files by Domain
Managing dozens of entities in a single file leads to frequent conflicts and reduced readability. ElastiCORE supports splitting blueprint files into multiple files. All .yml files inside the elasticore/ directory are automatically discovered.
Multi-Module Project Structure Example
project/
├── module-user/
│ └── src/main/resources/elasticore/
│ ├── env.yml
│ └── user-domain.yml # User, Role, Permission entities
├── module-order/
│ └── src/main/resources/elasticore/
│ ├── env.yml
│ └── order-domain.yml # Order, OrderItem, Payment entities
├── module-inventory/
│ └── src/main/resources/elasticore/
│ ├── env.yml
│ └── inventory-domain.yml # Product, Stock, Warehouse entities
└── module-common/
└── src/main/resources/elasticore/
└── common-domain.yml # Shared Enumerations, shared DTOs
Apply the ElastiCORE plugin independently in each module's build.gradle.
// module-order/build.gradle
plugins {
id 'io.elasticore' version '1.x.x'
}
elasticore {
basePackage = 'com.example.order'
blueprintPath = 'src/main/resources/elasticore'
}
Handling Cross-Domain References
When an entity in one module needs to reference an entity in another module, indirect references via DTOs are recommended. Using ID-based references instead of direct JPA associations reduces coupling between modules.
# module-order/order-domain.yml
dto:
OrderSummaryDTO:
meta: dto
fields:
orderId: long
userId: long # References only the ID of the User entity (no direct association)
userEmail: string # Required data is composed via API calls
totalAmount: decimal
Namespace Separation
Set a different base package per module to prevent class name collisions.
# module-user/env.yml
project:
name: user-module
basePackage: com.example.user
generatePath: src/main/java
# module-order/env.yml
project:
name: order-module
basePackage: com.example.order
generatePath: src/main/java
Define Enumerations and Value Objects shared across multiple domains in module-common, and configure other modules to include it as a dependency.
4. Custom Interfaces
Adding Custom Interfaces to Entities
ElastiCORE allows you to instruct generated entity classes to implement specific interfaces via the @implements(...) annotation.
entity:
User:
meta: entity @expose(50) @implements(Auditable, Serializable)
fields:
uid: long @id @sequence
email: string(100)! @unique
name: string(50)!
createdAt: localdatetime
updatedAt: localdatetime
Order:
meta: entity @expose(50) @implements(Serializable, Versionable)
fields:
oid: long @id @sequence
userId: long!
status: OrderStatus
version: int @version
Interface Implementation in Generated Entities
ElastiCORE generates entity classes with the declared interfaces added to the implements clause. The interfaces themselves are defined by the developer.
// Interface defined by the developer
public interface Auditable {
LocalDateTime getCreatedAt();
LocalDateTime getUpdatedAt();
void setCreatedAt(LocalDateTime createdAt);
void setUpdatedAt(LocalDateTime updatedAt);
}
// Entity generated by ElastiCORE (conceptual example)
@Entity
@Table(name = "ec_user")
public class User implements Auditable, Serializable {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long uid;
@Column(name = "email", length = 100, nullable = false, unique = true)
private String email;
// ... remaining fields and methods
}
Implementing the Auditable interface allows you to leverage automatic audit field population together with Spring Data's @EntityListeners(AuditingEntityListener.class). Serializable is required for Redis caching or session serialization.
5. Audit Logging
Using the @audited Annotation
To automatically track entity change history, add the @audited annotation to the meta declaration.
entity:
Product:
meta: entity @expose(50) @audited
fields:
pid: long @id @sequence
name: string(200)!
price: decimal!
stock: int
description: string(1000) @notaudited # Excluded from history tracking
JPA Envers Integration Guide
ElastiCORE's @audited integrates internally with Hibernate Envers. Add the dependency to build.gradle.
dependencies {
implementation 'org.springframework.data:spring-data-envers'
implementation 'org.hibernate.orm:hibernate-envers'
}
Generated audit tables have the _aud suffix appended to the original table name.
-- Table structure automatically generated by ElastiCORE + Envers
CREATE TABLE ec_product_aud (
pid BIGINT NOT NULL,
rev INTEGER NOT NULL,
revtype SMALLINT,
name VARCHAR(200),
price DECIMAL,
stock INTEGER,
PRIMARY KEY (pid, rev)
);
CREATE TABLE revinfo (
rev INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
revtstmp BIGINT
);
Querying Change History
// Query the full change history of a specific entity
@Service
@RequiredArgsConstructor
public class ProductAuditService {
private final AuditReader auditReader;
public List<ProductRevision> getProductHistory(Long productId) {
List<Number> revisions = auditReader.getRevisions(Product.class, productId);
return revisions.stream()
.map(rev -> {
Product snapshot = auditReader.find(Product.class, productId, rev);
RevisionInfo revInfo = auditReader.findRevision(RevisionInfo.class, rev);
return new ProductRevision(snapshot, rev, revInfo.getRevisionDate());
})
.collect(Collectors.toList());
}
public Product getProductAtRevision(Long productId, int revision) {
return auditReader.find(Product.class, productId, revision);
}
}
By implementing RevisionListener, you can store additional information such as the modifier's ID and IP address alongside the audit table entries.
6. Security Integration
Integration Pattern with Spring Security
The REST API endpoints generated by ElastiCORE integrate naturally with Spring Security configuration. Apply security rules to the APIs of entities exposed via the @expose annotation.
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
// Apply security rules to the API path patterns generated by ElastiCORE
.requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/products/**").hasRole("MANAGER")
.requestMatchers(HttpMethod.PUT, "/api/products/**").hasRole("MANAGER")
.requestMatchers(HttpMethod.DELETE, "/api/products/**").hasRole("ADMIN")
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
}
API Access Control
Method-level security enables fine-grained access control at the service layer. You can add @PreAuthorize as a custom extension to the service interfaces generated by ElastiCORE.
// Custom implementation that extends the generated service
@Service
public class SecureProductService extends GeneratedProductServiceImpl {
@Override
@PreAuthorize("hasRole('MANAGER') or #productId == authentication.principal.ownedProductId")
public ProductDTO updateProduct(Long productId, ProductUpdateDTO dto) {
return super.updateProduct(productId, dto);
}
@Override
@PreAuthorize("hasRole('ADMIN')")
public void deleteProduct(Long productId) {
super.deleteProduct(productId);
}
}
Handling Sensitive Data
Mark sensitive fields explicitly in the DSL to exclude them from serialization or apply masking.
entity:
User:
meta: entity @expose(50)
fields:
uid: long @id @sequence
email: string(100)! @unique
password: string(255)! @sensitive # Automatically excluded from JSON responses
name: string(50)!
phoneNumber: string(20) @sensitive # Subject to masking
ssn: string(14) @sensitive @encrypted # Encrypted at rest
// @sensitive fields are automatically excluded from the generated DTO
public class UserDTO {
private Long uid;
private String email;
private String name;
// password, phoneNumber, and ssn fields are not included
}
When using the @encrypted annotation, never hardcode encryption keys in your source code or configuration files. Use a key management service such as AWS KMS or HashiCorp Vault.
7. Caching Strategy
Using the @cache Annotation
Applying the @cache annotation to entities or methods that are read frequently but rarely modified automatically integrates with the Spring Cache abstraction.
transaction:
port:
ProductService:
meta: dbms @datasource("main")
methods:
getProductById:
meta: @cache("products")
params:
productId: long
return: ProductDTO
findActiveCategories:
meta: @cache("categories") @cacheTtl(3600) # 1-hour TTL
return: list(CategoryDTO)
updateProduct:
meta: @cacheEvict("products") # Invalidate cache on update
params:
productId: long
dto: ProductUpdateDTO
return: ProductDTO
Spring Cache Integration
A typical configuration for using Redis as a cache store.
// build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()
)
);
Map<String, RedisCacheConfiguration> cacheConfigurations = Map.of(
"products", defaultConfig.entryTtl(Duration.ofMinutes(60)),
"categories", defaultConfig.entryTtl(Duration.ofHours(3)),
"userProfiles", defaultConfig.entryTtl(Duration.ofMinutes(15))
);
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultConfig)
.withInitialCacheConfigurations(cacheConfigurations)
.build();
}
}
Cache Invalidation Strategy
// Explicitly manage cache invalidation scenarios
@Service
@RequiredArgsConstructor
public class ProductCacheManager {
private final CacheManager cacheManager;
// Evict only the cache entry for a specific product
public void evictProduct(Long productId) {
Cache productCache = cacheManager.getCache("products");
if (productCache != null) {
productCache.evict(productId);
}
}
// Clear the entire category cache (after an admin operation)
public void clearAllCategories() {
Cache categoryCache = cacheManager.getCache("categories");
if (categoryCache != null) {
categoryCache.clear();
}
}
}
When using Redis caching, the DTO classes generated by ElastiCORE must implement Serializable or be serializable by Jackson. Adding the @implements(Serializable) annotation to an entity applies this automatically to the generated classes.
Summary
| Feature | DSL Annotation | Key Integration |
|---|---|---|
| Multiple datasources | @datasource("name") | Spring Data JPA multi-DataSource |
| Custom interfaces | @implements(Interface) | Developer-defined interfaces |
| Audit logging | @audited, @notaudited | Hibernate Envers |
| Sensitive data exclusion | @sensitive | Jackson serialization control |
| Data encryption | @encrypted | JPA Converter |
| Caching | @cache, @cacheEvict | Spring Cache + Redis |
Apply enterprise features selectively based on your project requirements. Rather than enabling every feature from the start, introducing them incrementally as the actual need arises leads to better long-term maintainability.