Skip to main content

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.

Who this document is for

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}
Using environment variables

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
}
Distributed transaction caution

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
Common module design

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
}
Usage examples

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);
}
}
Extending audit information

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
}
Encryption key management

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();
}
}
}
Cache serialization requirements

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​

FeatureDSL AnnotationKey Integration
Multiple datasources@datasource("name")Spring Data JPA multi-DataSource
Custom interfaces@implements(Interface)Developer-defined interfaces
Audit logging@audited, @notauditedHibernate Envers
Sensitive data exclusion@sensitiveJackson serialization control
Data encryption@encryptedJPA Converter
Caching@cache, @cacheEvictSpring 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.