A comprehensive Java framework for building enterprise microservices with consistent architectural patterns.
- Overview
- Getting Started
- Architecture
- Core Patterns
- Temporal Workflows
- Entry Points
- Data Layer
- Validation
- Configuration
- Error Handling
- Modules
Trellis Framework provides a standardized architecture for building scalable microservices. Key features:
- Action/Task Pattern - Clean separation between use cases and atomic operations
- Stateless Design - All components are stateless for thread-safety
- Multi-Database Support - JPA, MongoDB, Elasticsearch
- Temporal Integration - Durable workflows for complex business processes
- Built-in Validation - FluentValidator for request validation
- Caching & Messaging - Redis, Kafka, RabbitMQ support
<parent>
<groupId>net.trellisframework</groupId>
<artifactId>boot</artifactId>
<version>LATEST</version>
</parent>
<dependencies>
<dependency>
<groupId>net.trellisframework</groupId>
<artifactId>context</artifactId>
</dependency>
<dependency>
<groupId>net.trellisframework</groupId>
<artifactId>data-sql</artifactId>
</dependency>
<dependency>
<groupId>net.trellisframework</groupId>
<artifactId>http</artifactId>
</dependency>
</dependencies>project-root/
├── pom.xml
├── libs/ # Shared Libraries
│ ├── lib-general/ # Configuration & utilities
│ ├── lib-sql/ # Database abstraction
│ └── lib-{domain}/ # Domain-specific library
│
└── apps/
└── {service-name}/ # Microservice Application
└── com.example.{service}
├── user/ # Feature module
│ ├── action/
│ ├── task/
│ ├── repository/
│ ├── model/
│ ├── payload/
│ ├── constant/
│ ├── job/
│ └── api/
└── common/ # Shared utilities
Each feature module follows this structure:
user/
├── action/ # Use case handlers
│ ├── AddUserAction.java
│ ├── ReadUserAction.java
│ ├── UpdateUserAction.java
│ ├── DeleteUserAction.java
│ └── ProcessUserAction.java
│
├── task/ # Atomic operations
│ ├── SaveUserTask.java
│ ├── FindUserByIdTask.java
│ ├── FindUserByEmailTask.java
│ └── SendWelcomeEmailTask.java
│
├── repository/ # Data access
│ └── UserRepository.java
│
├── model/ # JPA entities
│ └── UserEntity.java
│
├── payload/ # Request/Response DTOs
│ ├── AddUserRequest.java
│ ├── ReadUserRequest.java
│ ├── BrowseUserRequest.java
│ └── User.java
│
├── constant/ # Module constants
│ ├── Config.java
│ ├── Constant.java
│ └── Messages.java
│
├── job/ # Scheduled jobs
│ ├── ProcessUserJob.java
│ └── StuckUserJob.java
│
└── api/ # Entry points
├── rest/
│ └── UserUMController.java
├── grpc/
│ └── UserGMController.java
└── event/
└── UserEventHandler.java
┌──────────────────────────────────────────────────────────────────────────────┐
│ ENTRY POINTS │
│ REST Controller │ gRPC Controller │ Event Handler │ Job │
└─────────────────────────────────────┬────────────────────────────────────────┘
│
┌─────────────────────────────────────▼────────────────────────────────────────┐
│ ORCHESTRATOR LAYER │
│ ┌─────────────────────────────┐ ┌─────────────────────────────────────┐ │
│ │ Action │ │ WorkflowAction │ │
│ │ - Synchronous execution │ │ - Temporal Workflow (durable) │ │
│ │ - @Service annotation │ │ - @Workflow annotation │ │
│ └──────────────┬──────────────┘ └──────────────┬──────────────────────┘ │
│ │ │ │
│ │ │ │
│ │ │ │
│ ▼ ▼ │
├──────────────────────────────────────────────────────────────────────────────┤
│ TASK LAYER │
│ ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────────────┐ │
│ │ Task │ │ RepositoryTask │ │ WorkflowTask / │ │
│ │ - @Service │ │ - @Service │ │ WorkflowRepositoryTask │ │
│ │ - Atomic ops │ │ - DB operations │ │ - @Activity annotation │ │
│ └───────────────────┘ └─────────┬─────────┘ └─────────────┬─────────────┘ │
│ │ │ │
└───────────────────────────────────┼──────────────────────────┼───────────────┘
│ │
┌───────────────────────────────────▼──────────────────────────▼───────────────┐
│ DATA ACCESS LAYER │
│ GenericJpaRepository │
└──────────────────────────────────────────────────────────────────────────────┘
| Component | Can Call | Cannot Call |
|---|---|---|
| Action | Actions, Tasks, RepositoryTasks | WorkflowTasks, WorkflowActions |
| WorkflowAction | WorkflowTasks, WorkflowRepositoryTasks, child WorkflowActions | Actions, Tasks, RepositoryTasks |
| Task | Nothing (atomic) | Everything |
| RepositoryTask | Repository methods only | Actions, Tasks |
| WorkflowTask | Nothing (atomic) | Everything |
| WorkflowRepositoryTask | Repository methods only | Actions, Tasks |
| FluentValidator | Actions, Tasks, RepositoryTasks | WorkflowTasks, WorkflowActions |
| Controller/Job | Actions, Tasks, RepositoryTasks, WorkflowActions | WorkflowTasks |
| Pattern | Use Case |
|---|---|
| Action | Simple CRUD, synchronous operations, fast response |
| WorkflowAction | Long-running processes, retries needed, async pipelines |
| Job | Periodic maintenance, batch cleanup, scheduled tasks |
All Action, Task, RepositoryTask, WorkflowAction, and WorkflowTask classes must follow these rules:
- NO instance variables (fields) - Must be completely stateless
- NO additional methods - Only the
execute()method is allowed - NO
@Autowiredfields - Usecall()method for dependencies - NO helper/utility methods - Extract to separate utility classes
Why? Trellis manages these classes as stateless singletons. Adding state causes thread-safety issues.
// WRONG
@Service
public class ProcessUserAction implements Action1<User, ProcessUserRequest> {
private String tempValue; // FORBIDDEN - instance variable
@Autowired
private SomeService service; // FORBIDDEN - autowired field
private String helper() { ... } // FORBIDDEN - additional method
}
// CORRECT
@Service
public class ProcessUserAction implements Action1<User, ProcessUserRequest> {
@Override
public User execute(ProcessUserRequest request) {
String value = call(CalculateValueTask.class, request);
return call(CreateUserTask.class, value);
}
}Actions represent Use Cases - each Action handles one complete business operation.
Interfaces: Action0, Action1<R, P>, Action2<R, P1, P2> ... up to Action5
Naming Convention:
Add{Entity}Action- Create operationsRead{Entity}Action- Single readFind{Entity}By{Criteria}Action- SearchUpdate{Entity}Action- UpdateDelete{Entity}Action- DeleteProcess{Entity}Action- Complex processingBrowse{Entity}Action- List/paginated
@Service
public class AddUserAction implements Action1<User, AddUserRequest> {
@Override
public User execute(AddUserRequest request) {
UserEntity entity = UserEntity.of(request);
entity = call(SaveUserTask.class, entity);
call(SendWelcomeEmailTask.class, entity);
return plainToClass(entity, User.class);
}
}Action Composition:
@Override
public User execute(AddUserRequest request) {
// Call another action
UserProfile profile = call(GetDefaultProfileAction.class);
// Call a task
UserEntity entity = call(SaveUserTask.class, UserEntity.of(request, profile));
// Async call
CompletableFuture.runAsync(() -> call(SendNotificationAction.class, entity));
return plainToClass(entity, User.class);
}Tasks are single-purpose operations - each Task does exactly ONE thing.
Interfaces: Task0, Task1<R, P>, Task2<R, P1, P2> ... up to Task5
Naming Convention:
Notify{Event}Task- NotificationsCalculate{Value}Task- ComputationsTransform{Data}Task- Data transformationValidate{Entity}Task- ValidationSend{Message}Task- External communication
@Service
public class SendWelcomeEmailTask implements Task1<Void, UserEntity> {
@Override
public Void execute(UserEntity user) {
emailClient.send(
user.getEmail(),
"Welcome!",
"Hello " + user.getName()
);
return null;
}
}Tasks cannot call anything - they are atomic and isolated.
RepositoryTasks are single database operations with access to ONE repository.
Interface: RepositoryTask1<Repository, ReturnType, InputType>
Naming Convention:
Save{Entity}Task- Save single entitySaveAll{Entity}Task- Save multiple entitiesFind{Entity}By{Criteria}Task- Find operationsUpdate{Entity}By{Criteria}Task- UpdatesDelete{Entity}Task- DeleteCount{Entity}By{Criteria}Task- CountExists{Entity}By{Criteria}Task- Existence check
@Service
public class SaveUserTask implements RepositoryTask1<UserRepository, UserEntity, UserEntity> {
@Override
public UserEntity execute(UserEntity entity) {
return getRepository().save(entity);
}
}@Service
public class FindUserByIdTask implements RepositoryTask1<UserRepository, Optional<UserEntity>, String> {
@Override
public Optional<UserEntity> execute(String id) {
return getRepository().findById(id);
}
}Notes:
- RepositoryTasks can only call repository methods via
getRepository() - Default transaction:
@Transactional(propagation = REQUIRES_NEW, rollbackFor = Exception.class) - Only add
@Transactionalannotation if you need different behavior
Repositories extend GenericJpaRepository with QueryDSL support.
@Repository
public interface UserRepository extends GenericJpaRepository<UserEntity, String> {
Optional<UserEntity> findByEmail(String email);
default List<UserEntity> findPendingUsers(int limit) {
QUserEntity entity = QUserEntity.userEntity;
return getFactory().selectFrom(entity)
.where(entity.state.eq(State.PENDING))
.orderBy(entity.created.asc())
.limit(limit)
.fetch();
}
default List<UserEntity> findUsersForProcessing(int limit) {
QUserEntity entity = QUserEntity.userEntity;
return getFactory().selectFrom(entity)
.where(entity.state.eq(State.PENDING))
.orderBy(entity.priority.desc(), entity.created.asc())
.limit(limit)
.setLockMode(LockModeType.PESSIMISTIC_WRITE)
.setHint("jakarta.persistence.lock.timeout", -2)
.fetch();
}
}Lock Timeout Values:
-2= NO_WAIT (immediate fail if locked)-1= Wait indefinitelyn= Wait n milliseconds
For long-running or complex business processes, use Temporal integration.
Equivalent to a Temporal Workflow - orchestrates the entire business process.
@Async
@Workflow(executionTimeout = "2h", version = "1.0.0")
public class ProcessOrderWorkflowAction implements WorkflowAction1<Order, ProcessOrderRequest> {
@Override
public Order execute(ProcessOrderRequest request) {
// Step 1: Validate
Boolean isValid = call(ValidateOrderTask.class, request);
if (!Boolean.TRUE.equals(isValid)) {
return Order.failed(request.getRefId(), Messages.ORDER_INVALID);
}
// Step 2: Reserve inventory
Boolean reserved = call(ReserveInventoryTask.class, request.getItems());
if (!Boolean.TRUE.equals(reserved)) {
return Order.failed(request.getRefId(), Messages.INVENTORY_UNAVAILABLE);
}
// Step 3: Process payment
PaymentResult payment = call(ProcessPaymentTask.class, request.getPayment());
if (!payment.isSuccess()) {
call(ReleaseInventoryTask.class, request.getItems());
return Order.failed(request.getRefId(), Messages.PAYMENT_FAILED);
}
// Step 4: Save order
OrderEntity entity = call(SaveOrderTask.class, OrderEntity.of(request, payment));
// Step 5: Async notification
Optional.ofNullable(request.getWebhookUrl())
.filter(StringUtils::isNotBlank)
.ifPresent(url -> callAsync(NotifyWebhookWorkflowAction.class, WebhookRequest.of(url, entity)));
return plainToClass(entity, Order.class);
}
}@Workflow Annotation:
| Attribute | Default | Description |
|---|---|---|
taskQueue |
"" | Temporal task queue name |
executionTimeout |
"" | Maximum workflow execution time |
runTimeout |
"" | Maximum single run time |
taskTimeout |
"" | Maximum workflow task time |
version |
"0.0.0" | Workflow version for versioning |
@Async Annotation: Add for non-blocking execution. API returns immediately with workflow ID.
Equivalent to a Temporal Activity - executes a single atomic operation.
@Activity(
retry = @Retry(maxAttempts = 3, backoff = @Backoff(delay = 1000, maxDelay = 30000, multiplier = 2.0)),
startToCloseTimeout = "30s"
)
public class ValidateOrderTask implements WorkflowTask1<Boolean, ProcessOrderRequest> {
@Override
public Boolean execute(ProcessOrderRequest request) {
return Optional.ofNullable(request)
.filter(r -> StringUtils.isNotBlank(r.getCustomerId()))
.filter(r -> r.getItems() != null && !r.getItems().isEmpty())
.filter(r -> r.getPayment() != null)
.isPresent();
}
}@Activity Annotation:
| Attribute | Default | Description |
|---|---|---|
startToCloseTimeout |
"60s" | Max time from activity start to completion |
scheduleToStartTimeout |
"" | Max time from schedule to start |
scheduleToCloseTimeout |
"" | Max time from schedule to completion |
heartbeat |
"10s" | Heartbeat interval for long-running activities |
retry |
@Retry | Retry configuration |
@Retry Annotation:
| Attribute | Default | Description |
|---|---|---|
maxAttempts |
1 | Maximum retry attempts |
backoff |
@Backoff | Backoff configuration |
include |
{} | Exception types to retry |
exclude |
{} | Exception types to not retry |
@Backoff Annotation:
| Attribute | Default | Description |
|---|---|---|
delay |
1000 | Initial delay (ms) |
maxDelay |
60000 | Maximum delay (ms) |
multiplier |
2.0 | Exponential multiplier |
Temporal Activity with database access.
@Activity
public class SaveOrderTask implements WorkflowRepositoryTask1<OrderRepository, OrderEntity, OrderEntity> {
@Override
public OrderEntity execute(OrderEntity entity) {
return getRepository().save(entity);
}
}Interface Parameter Order: WorkflowRepositoryTask1<Repository, ReturnType, InputType>
Note: Default transaction is @Transactional(propagation = REQUIRES_NEW, rollbackFor = Exception.class)
| Method | Description |
|---|---|
call(Task.class, args) |
Execute activity synchronously |
call(WorkflowAction.class, args, WorkflowOption) |
Execute child workflow with options |
callAsync(WorkflowAction.class, args) |
Start child workflow asynchronously |
callAsync(WorkflowAction.class, args, WorkflowOption) |
Start child workflow with options |
sleep(Duration) |
Temporal-safe sleep |
sleepMinutes(long) |
Sleep for minutes |
getWorkflowId() |
Get workflow execution ID |
getAttempt() |
Get current retry attempt |
version(changeId, maxVersion) |
Version-based branching |
Configure child workflow execution with WorkflowOption:
@Workflow(executionTimeout = "60m")
public class AddVerificationWorkflowAction implements WorkflowAction1<Verification, AddVerificationRequest> {
@Override
public Verification execute(AddVerificationRequest request) {
VerificationEntity entity = call(SaveVerificationTask.class, VerificationEntity.of(request));
// Start child workflow with custom ID
callAsync(ProcessEmailVerificationWorkflowAction.class, entity,
WorkflowOption.of("process-" + entity.getId()));
return plainToClass(entity, Verification.class);
}
}WorkflowOption:
| Factory Method | Description |
|---|---|
WorkflowOption.of(String id) |
Set workflow ID |
WorkflowOption.of(int priority) |
Set priority (1-5) |
WorkflowOption.of(String id, int priority) |
Set ID and priority |
@RestController
@RequestMapping(value = "/um/users", produces = MediaType.APPLICATION_JSON_VALUE)
@Validated
public class UserUMController implements Api {
@PreAuthorize("hasAnyRole('UsrUR', 'UsrUM')")
@GetMapping("/{id}")
public ResponseEntity<User> read(@PathVariable String id) {
return ResponseEntity.ok(call(ReadUserAction.class, id));
}
@PreAuthorize("hasAnyRole('UsrUA', 'UsrUM')")
@PostMapping
public ResponseEntity<User> add(@Validated @RequestBody AddUserRequest request) {
return ResponseEntity
.status(HttpStatus.CREATED)
.body(call(AddUserAction.class, request));
}
}With Temporal Workflow:
@RestController
@RequestMapping(value = "/um/orders", produces = MediaType.APPLICATION_JSON_VALUE)
public class OrderUMController implements Api, Workflow {
@PostMapping
public ResponseEntity<Order> create(@Validated @RequestBody CreateOrderRequest request) {
OrderEntity entity = call(SaveOrderTask.class, OrderEntity.of(request));
callAsync(ProcessOrderWorkflowAction.class, ProcessOrderRequest.of(entity.getId()));
return ResponseEntity
.status(HttpStatus.ACCEPTED)
.body(plainToClass(entity, Order.class));
}
}Controller Naming:
{Entity}UMController- User Management (personal endpoints){Entity}SMController- System Management (admin endpoints){Entity}GMController- Global Management (gRPC)
URL Convention:
/um/{resource}- User Management/sm/{resource}- System Management/v1/{resource}- Public API (versioned)
Role Naming: {Domain}{Scope}{Permission}
- Domain:
Usr,Ord, etc. - Scope:
U(User),S(System),G(Global) - Permission:
R(Read),A(Add),E(Edit),D(Delete),M(Manage)
Example: UsrUR = User-scope User Read
@GrpcController
public class UserGMController extends UserServiceGrpc.UserServiceImplBase implements Api {
@Override
public void getUser(GetUserRequest request, StreamObserver<UserResponse> responseObserver) {
User user = call(GetUserByIdAction.class, request.getId());
responseObserver.onNext(toProto(user));
responseObserver.onCompleted();
}
}@Component
public class UserEventHandler implements GenericEventController {
@CloudFunctionEventHandler(topic = "user-created")
public void onUserCreated(UserCreatedEvent event) {
call(ProcessNewUserAction.class, event);
}
}@Component
public class ProcessUsersJob extends Job {
@DistributedLock(value = "PROCESS_USERS_JOB", skipIfLocked = true, cooldown = "20s")
@Scheduled(fixedDelay = 1000)
public void execute() {
try {
call(ProcessPendingUsersAction.class);
} catch (Throwable t) {
Logger.error("ProcessUsersJob", t.getMessage());
}
}
}Job Naming:
Process{Entity}Job- Main processingStuck{Entity}Job- Recovery job
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
@Entity
@Table(name = "T_USER",
indexes = {
@Index(name = "idx_user_email", columnList = "email"),
@Index(name = "idx_user_state", columnList = "state")
})
public class UserEntity extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Column(name = "email", unique = true)
private String email;
@Column(name = "name")
private String name;
@Column(name = "state", nullable = false)
@Convert(converter = Transformers.StateToInteger.class)
private State state = State.PENDING;
@Column(name = "priority")
private Integer priority;
@Embedded
private Address address;
@Embedded
private Retry retry;
}Rules:
- Extend
BaseEntity(provides id, created, modified, version) - Table names:
T_{ENTITY_NAME}in uppercase - Use
@AllArgsConstructor(staticName = "of")for factory methods - Use
@Convertfor enum persistence - Define indexes for frequently queried columns
@Getter
@AllArgsConstructor
public enum State implements IEnumerated<Integer> {
PENDING(0),
IN_PROGRESS(1),
DONE(2),
FAILED(3),
CANCELLED(4);
private final Integer db;
}
public class Transformers {
@Converter
public static class StateToInteger extends GenericEnumConverter<State, Integer> {
public StateToInteger() {
super(State.class);
}
}
}PENDING → IN_PROGRESS → DONE
↘ FAILED
↘ CANCELLED
↓
QUEUED (for retry)
| State | Description |
|---|---|
PENDING |
Waiting to be processed |
QUEUED |
Temporarily delayed |
IN_PROGRESS |
Currently processing |
DONE |
Successfully completed |
FAILED |
Failed after max retries |
CANCELLED |
Cancelled by system/user |
Use annotations for static rules and FluentValidator for dynamic rules that require database lookups, external validation, or complex business logic.
@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class AddUserRequest implements Payload, FluentValidator<AddUserRequest> {
@Required // Static rule - use annotation
@Email // Static rule - use annotation
private String email;
@Required
private String name;
private String refId;
private Customer customer;
@Override
public void execute() {
// Dynamic rule: Check if email already exists in database (RepositoryTask)
addRule(
x -> call(ExistsUserByEmailTask.class, x.getEmail()),
() -> new ConflictException(Messages.EMAIL_ALREADY_EXISTS)
)
// Dynamic rule: Validate API key (Task)
.addRule(
x -> !call(ValidateApiKeyTask.class, x.getApiKey()),
() -> new UnauthorizedException(Messages.INVALID_API_KEY)
)
// Dynamic rule: Check quota (Action)
.addRule(
x -> !call(CheckQuotaAction.class, x.getCustomer().getId()),
() -> new ForbiddenException(Messages.QUOTA_EXCEEDED)
)
// Auto-populate: Set default values
.addRule(
x -> StringUtils.isBlank(x.getRefId()),
x -> x.setRefId(UUID.randomUUID().toString())
)
// Auto-populate: Set customer from JWT token
.addRule(x -> x.setCustomer(Customer.of(
OAuthSecurityContext.getPrincipalId(),
OAuthSecurityContext.getEmail()
)));
}
}
// Note: In FluentValidator you can only call Action, Task, and RepositoryTask (not WorkflowTask)When to Use Each:
| Validation Type | Approach |
|---|---|
| Required fields, format, length | Use annotations (@Required, @Email, @Size) |
| Database lookups (duplicate check) | Use FluentValidator with call() |
| External API validation | Use FluentValidator with call() |
| Business rules with conditions | Use FluentValidator |
| Auto-populate default values | Use FluentValidator with setter |
addRule Patterns:
| Pattern | Usage |
|---|---|
addRule(condition, () -> new Exception()) |
Throw exception if condition is true |
addRule(condition, x -> x.setField(value)) |
Set field if condition is true |
addRule(x -> x.setField(computed)) |
Always set field (auto-populate) |
Add{Entity}Request- Create requestRead{Entity}Request- Read requestBrowse{Entity}Request- List/search requestUpdate{Entity}Request- Update request{Entity}- Response DTO
Never use "Workflow" in payload names.
@Getter
@AllArgsConstructor
public enum Config implements IConfig {
MAX_RETRY("user.max-retry", "3"),
BATCH_SIZE("user.batch-size", "100"),
TIMEOUT("user.timeout", "30");
private final String property;
private final String defaultValue;
@Override
public String getProperty() {
return property;
}
@Override
public String getDefaultValue() {
return defaultValue;
}
}
// Usage
int maxRetry = Integer.parseInt(Config.MAX_RETRY.get());Always use Trellis HTTP exceptions:
| Exception | HTTP Status | Usage |
|---|---|---|
BadRequestException |
400 | Validation errors |
UnauthorizedException |
401 | Authentication required |
ForbiddenException |
403 | Access denied |
NotFoundException |
404 | Resource not found |
ConflictException |
409 | Duplicate resource |
InternalServerException |
500 | Server errors |
Never hardcode messages. Create a Messages enum:
public enum Messages implements MessageHandler {
USER_NOT_FOUND,
EMAIL_ALREADY_EXISTS,
EMAIL_IS_REQUIRED,
INVALID_EMAIL_FORMAT,
MAX_RETRY_EXCEEDED
}Usage:
// In Action
return call(FindUserByIdTask.class, id)
.orElseThrow(() -> new NotFoundException(Messages.USER_NOT_FOUND));
// In Validator
addRule(
x -> StringUtils.isBlank(x.getEmail()),
() -> new BadRequestException(Messages.EMAIL_IS_REQUIRED)
);
// WRONG - Never hardcode
throw new NotFoundException("User not found");| Module | Description |
|---|---|
core |
Base utilities, logging, constants |
context |
Action/Task/Process framework |
http |
REST client, HTTP exceptions |
data-sql |
JPA/Hibernate with QueryDSL |
data-mongo |
MongoDB support |
data-elastic |
Elasticsearch support |
cache-redis |
Redis caching |
cache-caffeine |
In-memory caching |
stream-kafka |
Kafka messaging |
stream-rabbit |
RabbitMQ messaging |
oauth-resource-keycloak |
Keycloak OAuth2 |
workflow-temporal |
Temporal workflows |
validator |
Custom validators |
util |
AWS, Crypto, Export, JWT |
message-mail |
Email services |
socket-websocket |
WebSocket support |
| Component | Technology |
|---|---|
| Framework | Spring Boot 4.x |
| Cloud | Spring Cloud 2024.x |
| Language | Java 21 |
| Build | Maven 3 |
| Database | PostgreSQL, Oracle |
| Cache | Redis |
| ORM | JPA + Hibernate + QueryDSL |
| Workflow | Temporal |
| Container | Docker (JIB Plugin) |
| Variable | Default | Description |
|---|---|---|
SERVER_PORT |
80 | API port |
ACTUATOR_PORT |
8080 | Management port |
APPLICATION_MODE |
production | Profile |
CONFIG_SERVER_URI |
http://config | Config Server |
mvn package -P production- Use Lombok for boilerplate reduction
- Use
Optionalfor nullable returns - never return null - Use
@AllArgsConstructor(staticName = "of")for factory methods - Chain with Optional for null-safe operations
- Use early returns to reduce nesting
- No comments in code - code should be self-explanatory
Optional.ofNullable(entity.getStatus())
.filter(Status::isActive)
.ifPresent(x -> processActive(entity));Logger.info("User created: {}", user.getId());
Logger.error("Failed to process user", exception);
// Performance logging
Logger.info(
() -> heavyOperation(),
(time, result) -> time > 1000,
(time, result) -> String.format("Operation took %d ms", time)
);Use @CacheableConfig to configure cache behavior per method:
@CacheableConfig(value = "USER_CACHE", ttl = "1h", serializer = CacheSerializer.JSON)
@Cacheable(cacheNames = "USER_CACHE", key = "#email")
public User findByEmail(String email) { ... }@CacheableConfig Annotation:
| Attribute | Default | Description |
|---|---|---|
value |
{} | Cache names |
ttl |
"" | Time-to-live (e.g., "1h", "30m", "1d") |
serializer |
JDK | Serialization: JDK, BYTE_ARRAY, STRING, JSON |
@DistributedLock(value = "PROCESS_USERS", skipIfLocked = true, cooldown = "20s")
@Scheduled(fixedDelay = 1000)
public void execute() { ... }MIT License
Contributions are welcome! Please read our contributing guidelines.
- GitHub Issues: Report a bug