Skip to the content.

Trellis Framework

A comprehensive Java framework for building enterprise microservices with consistent architectural patterns.

Java 21 Spring Boot 4.0 Spring Cloud Temporal License


Table of Contents


Introduction

Trellis Framework is an opinionated, full-featured Java framework built on top of Spring Boot 4.0 and Spring Cloud 2025. It provides a standardized architecture for building enterprise-grade microservices with consistent patterns across all layers of your application.

Key Features:


Getting Started

Requirements

Requirement Version
Java 21+
Maven 3.9+
Spring Boot 4.0.1
Spring Cloud 2025.1.0

Installation

Add Trellis as the parent POM and include the modules you need:

<parent>
    <groupId>net.trellisframework</groupId>
    <artifactId>boot</artifactId>
    <version>LATEST</version>
</parent>

<dependencies>
    <!-- Core framework (required) -->
    <dependency>
        <groupId>net.trellisframework</groupId>
        <artifactId>context</artifactId>
    </dependency>

    <!-- Database access -->
    <dependency>
        <groupId>net.trellisframework</groupId>
        <artifactId>data-sql</artifactId>
    </dependency>

    <!-- HTTP client & exceptions -->
    <dependency>
        <groupId>net.trellisframework</groupId>
        <artifactId>http</artifactId>
    </dependency>

    <!-- Validation annotations -->
    <dependency>
        <groupId>net.trellisframework</groupId>
        <artifactId>validator</artifactId>
    </dependency>

    <!-- Add more modules as needed -->
</dependencies>

Minimal setup requires only context. Add other modules based on your needs:

Need Module
SQL database (PostgreSQL, Oracle) data-sql
MongoDB data-mongo
Elasticsearch data-elastic
Redis caching & distributed locks cache-redis
In-memory caching cache-caffeine
Kafka messaging stream-kafka
RabbitMQ messaging stream-rabbit
OAuth2 / Keycloak oauth-resource-keycloak
Temporal workflows workflow-temporal
HTTP client (Retrofit) http
Custom validators validator
Email services message
WebSocket socket-websocket
Utilities (AWS, Crypto, JWT, Export) util

Project Structure

A typical Trellis microservice project follows this layout:

project-root/
├── pom.xml                              # Parent POM inheriting from Trellis
│
├── libs/                                # Shared Libraries
│   ├── lib-general/                     # Configuration & shared utilities
│   │   └── com.example.lib.general
│   │       ├── config/                  # IConfig implementations
│   │       ├── constant/                # Shared constants
│   │       └── util/                    # Utility classes
│   │
│   ├── lib-sql/                         # Database abstraction layer
│   │   └── com.example.lib.sql
│   │       ├── model/                   # BaseEntity, embeddables
│   │       └── transform/              # Enum converters
│   │
│   └── lib-{domain}/                    # Domain-specific shared library
│       └── com.example.lib.{domain}
│           ├── common/                  # Shared enums, models
│           ├── domain/                  # Domain models
│           └── util/                    # Domain utilities
│
└── apps/
    └── {service-name}/                  # Microservice Application
        └── com.example.{service}
            ├── user/                    # Feature module
            ├── order/                   # Feature module
            └── common/                  # Shared service utilities

Module Structure

Each feature module follows a consistent, predictable structure:

user/
├── action/                              # Use case handlers (orchestrators)
│   ├── AddUserAction.java
│   ├── ReadUserAction.java
│   ├── UpdateUserAction.java
│   ├── DeleteUserAction.java
│   ├── BrowseUserAction.java
│   └── ProcessUserAction.java
│
├── task/                                # Atomic operations
│   ├── SaveUserTask.java
│   ├── FindUserByIdTask.java
│   ├── FindUserByEmailTask.java
│   ├── ExistsUserByEmailTask.java
│   └── SendWelcomeEmailTask.java
│
├── repository/                          # Data access interfaces
│   └── UserRepository.java
│
├── model/                               # JPA entities & embeddables
│   └── UserEntity.java
│
├── payload/                             # Request/Response DTOs
│   ├── AddUserRequest.java
│   ├── ReadUserRequest.java
│   ├── BrowseUserRequest.java
│   ├── UpdateUserRequest.java
│   └── User.java                        # Response DTO
│
├── constant/                            # Module-level constants
│   ├── Config.java                      # Configuration keys
│   ├── Constant.java                    # Static constants
│   └── Messages.java                    # Error/info message keys
│
├── transformer/                         # Enum & type converters
│   └── Transformers.java
│
├── job/                                 # Scheduled background jobs
│   ├── ProcessUserJob.java
│   └── StuckUserJob.java
│
└── api/                                 # Entry points
    ├── rest/                            # REST controllers
    │   ├── UserUMController.java        # User Management
    │   └── UserSMController.java        # System Management
    ├── grpc/                            # gRPC controllers
    │   └── UserGMController.java
    ├── event/                           # Cloud event handlers
    │   └── UserEventHandler.java
    └── web/                             # Web (SSR) controllers
        └── UserWebController.java

Architecture

Layer Overview

Trellis enforces a strict layered architecture with clear boundaries between components:

┌─────────────────────────────────────────────────────────────────────┐
│                          ENTRY POINTS                                │
│   REST  │  gRPC  │  Event Handler  │  Web Controller  │  Job        │
└────────────────────────────┬────────────────────────────────────────┘
                             │
┌────────────────────────────▼────────────────────────────────────────┐
│                      ORCHESTRATOR LAYER                              │
│  ┌──────────────────────┐       ┌────────────────────────────────┐  │
│  │       Action         │       │        WorkflowAction          │  │
│  │  - @Service          │       │  - @Workflow                   │  │
│  │  - Synchronous       │       │  - Temporal (durable/async)    │  │
│  └──────────┬───────────┘       └──────────────┬─────────────────┘  │
│             │                                  │                    │
└─────────────┼──────────────────────────────────┼────────────────────┘
              │                                  │
┌─────────────▼──────────────────────────────────▼────────────────────┐
│                         TASK LAYER                                    │
│  ┌──────────────┐  ┌──────────────────┐  ┌────────────────────────┐ │
│  │    Task      │  │  RepositoryTask  │  │  WorkflowTask /        │ │
│  │  - @Service  │  │  - @Service      │  │  WorkflowRepositoryTask│ │
│  │  - Atomic    │  │  - DB operations │  │  - @Activity           │ │
│  └──────────────┘  └────────┬─────────┘  └───────────┬────────────┘ │
│                              │                        │              │
└──────────────────────────────┼────────────────────────┼──────────────┘
                               │                        │
┌──────────────────────────────▼────────────────────────▼──────────────┐
│                       DATA ACCESS LAYER                               │
│  GenericJpaRepository  │  GenericMongoRepository  │  GenericElastic.. │
└─────────────────────────────────────────────────────────────────────┘

Standard Architecture (Synchronous)

For typical CRUD and synchronous operations:

┌──────────────────────────────────────────────────────────────────┐
│                         Entry Points                              │
│  REST  │  gRPC  │  Event  │  Web  │  Job                         │
└───────────────────────┬──────────────────────────────────────────┘
                        │
                        ▼
┌───────────────────────────────────────────────────────────────────┐
│                      Action Layer                                  │
│  - Use case handlers (orchestrators)                               │
│  - Can call: other Actions, Tasks, RepositoryTasks                 │
│  - CANNOT call: WorkflowTasks, WorkflowRepositoryTasks             │
└───────────────────────┬──────────────────────────────────────────┘
                        │
                        ▼
┌───────────────────────────────────────────────────────────────────┐
│                  Task / RepositoryTask Layer                       │
│  - Task: Atomic operations (one operation per task)                │
│  - RepositoryTask: Database operations via getRepository()         │
│  - Can call: NOTHING (isolated and atomic)                         │
└───────────────────────┬──────────────────────────────────────────┘
                        │
                        ▼
┌───────────────────────────────────────────────────────────────────┐
│                     Repository Layer                               │
│  - GenericJpaRepository with QueryDSL                              │
│  - GenericMongoRepository                                          │
│  - GenericElasticRepository                                        │
└───────────────────────────────────────────────────────────────────┘
        │                       │                       │
        ▼                       ▼                       ▼
   PostgreSQL              MongoDB              Elasticsearch

Temporal Architecture (Async)

For long-running, durable business processes:

┌──────────────────────────────────────────────────────────────────┐
│                         Entry Points                              │
│  REST Controller  │  gRPC Controller  │  Event Handler            │
└───────────────────────┬──────────────────────────────────────────┘
                        │
                        ▼
┌───────────────────────────────────────────────────────────────────┐
│                   WorkflowAction Layer                             │
│  - Temporal Workflow (durable, survives restarts)                  │
│  - Can call: ALL Task types + child WorkflowActions                │
│  - Annotations: @Workflow, @Async                                  │
└───────────────────────┬──────────────────────────────────────────┘
                        │
          ┌─────────────┼─────────────┐
          ▼                           ▼
┌─────────────────────┐  ┌───────────────────────────────────────┐
│  Shared Tasks       │  │  Temporal-Only Tasks                   │
│  Task               │  │  WorkflowTask (@Activity)              │
│  RepositoryTask     │  │  WorkflowRepositoryTask (@Activity)    │
│  (@Service)         │  │  - Supports @Retry, @Backoff           │
└─────────────────────┘  └───────────────────────────────────────┘
                        │
                        ▼
                 Temporal Server
              (Workflow State Store)

Callable Rules

Component Can Call Cannot Call
Action Actions, Tasks, RepositoryTasks WorkflowTasks, WorkflowActions
WorkflowAction WorkflowTasks, WorkflowRepositoryTasks, Tasks, RepositoryTasks, child WorkflowActions Actions
Task Nothing (atomic) Everything
RepositoryTask Repository methods only (via getRepository()) Actions, Tasks
WorkflowTask Nothing (atomic) Everything
WorkflowRepositoryTask Repository methods only (via getRepository()) Actions, Tasks
FluentValidator Actions, Tasks, RepositoryTasks WorkflowTasks, WorkflowActions
Controller / Job Actions, Tasks, RepositoryTasks, WorkflowActions WorkflowTasks

When to Use Each Pattern

Pattern Use Case Example
Action Simple CRUD, synchronous operations, fast response Adding a user, reading an order
WorkflowAction Long-running processes, retries, async pipelines Payment processing, email verification
Job Periodic maintenance, batch cleanup, scheduled tasks Stuck record recovery, data sync

Core Components

Critical Rules

All Action, Task, RepositoryTask, WorkflowAction, WorkflowTask, and WorkflowRepositoryTask classes must follow these rules:

  1. NO instance variables (fields) — Must be completely stateless
  2. NO additional methods — Only the execute() method is allowed
  3. NO @Autowired fields — Use call() method for dependencies
  4. NO helper/utility methods — Extract to separate utility classes

Why? Trellis manages these classes as stateless singletons. Adding state causes thread-safety issues in concurrent processing.

// WRONG - Violates all rules
@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 - Stateless, single execute() method
@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);
    }
}

Action

Actions represent Use Cases — each Action handles one complete business operation by orchestrating Tasks.

Interfaces: Action (no args), Action1<R, P>, Action2<R, P1, P2> … up to Action5<R, P1, P2, P3, P4, P5>

Annotation: @Service

Naming Convention:

Prefix Operation
Add{Entity}Action Create
Read{Entity}Action Single read
Browse{Entity}Action List / paginated
Find{Entity}By{Criteria}Action Search by criteria
Update{Entity}Action Update
Delete{Entity}Action Delete
Process{Entity}Action Complex processing

Example:

@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);
    }
}

Composition — calling other Actions and Tasks:

@Override
public User execute(AddUserRequest request) {
    // Call another action
    UserProfile profile = call(GetDefaultProfileAction.class);

    // Call a repository task
    UserEntity entity = call(SaveUserTask.class, UserEntity.of(request, profile));

    // Async fire-and-forget
    CompletableFuture.runAsync(() -> call(SendNotificationAction.class, entity));

    return plainToClass(entity, User.class);
}

Task

Tasks are single-purpose atomic operations — each Task does exactly ONE thing. Tasks cannot call anything else.

Interfaces: Task (no args), Task1<R, P>, Task2<R, P1, P2> … up to Task5

Annotation: @Service

Naming Convention:

Prefix Operation
Notify{Event}Task Send notifications
Calculate{Value}Task Compute values
Transform{Data}Task Data transformation
Validate{Entity}Task Business validation
Send{Message}Task External communication
Call{Service}Task External API calls

Example:

@Service
public class SendWelcomeEmailTask implements Task1<Void, UserEntity> {

    @Override
    public Void execute(UserEntity user) {
        emailClient.send(user.getEmail(), "Welcome!", "Hello " + user.getName());
        return null;
    }
}

RepositoryTask

RepositoryTasks are single database operations with access to exactly ONE repository.

Interface: RepositoryTask1<Repository, ReturnType, InputType> (up to RepositoryTask5)

Annotation: @Service

Default Transaction: @Transactional(propagation = REQUIRES_NEW, rollbackFor = Exception.class)

Naming Convention:

Prefix Operation
Save{Entity}Task Save single entity
SaveAll{Entity}Task Save collection
Find{Entity}By{Criteria}Task Find operations
Update{Entity}By{Criteria}Task Updates
Delete{Entity}Task Delete
Count{Entity}By{Criteria}Task Count
Exists{Entity}By{Criteria}Task Existence check

Examples:

@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);
    }
}

@Service
public class ExistsUserByEmailTask implements RepositoryTask1<UserRepository, Boolean, String> {

    @Override
    public Boolean execute(String email) {
        return getRepository().findByEmail(email).isPresent();
    }
}

Repository

Repositories extend GenericJpaRepository with built-in QueryDSL support.

Annotation: @Repository

Base Interface: GenericJpaRepository<TEntity, ID> — extends JpaRepository, QuerydslPredicateExecutor

Built-in Methods from GenericJpaRepository:

Method Description
getFactory() Get JPAQueryFactory for QueryDSL queries
getEntityManager() Get JPA EntityManager
select(expressions) Start a select query
selectFrom(entity) Select from entity
selectDistinct(expressions) Distinct select
update(entity) Update query
delete(entity) Delete query
insert(entity) Insert query
upsert(entity, columns) Insert or update operations
findAll(predicate, pageable) Paginated find with predicate

Example:

@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();
    }
}

Query Method Naming:


Entity

JPA entities extend BaseEntity which provides common fields.

Base Fields (from BaseEntity): id, created, modified, version

Rules:

Rule Convention
Inheritance Extend BaseEntity
Table name T_{ENTITY_NAME} in uppercase
Factory method @AllArgsConstructor(staticName = "of")
Enum persistence @Convert(converter = Transformers.XToY.class)
Indexes @Index for frequently queried columns

Example:

@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;
}

Payload

Payloads are Request/Response DTOs that implement the Payload interface.

Naming Convention:

Name Purpose
Add{Entity}Request Create request
Read{Entity}Request Read request
Browse{Entity}Request List / search request
Update{Entity}Request Update request
{Entity} Response DTO

Never use “Workflow” in payload names — payloads are shared across sync and async contexts.

Example — Request:

@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class AddUserRequest implements Payload {

    @Required
    @Email
    private String email;

    @Required
    private String name;

    private String refId;
}

Example — Response:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Payload {
    private String id;
    private String email;
    private String name;
    private State state;
    private Date created;
}

Entry Points

Entry points are the external interfaces of your microservice. Trellis supports five types:

Type Annotation Location Interface
REST @RestController api/rest/ Api
gRPC @GrpcController api/grpc/ Api
Event @Component api/event/ GenericEventController
Web (SSR) @Controller api/web/ GenericController
Job @Component job/ extends Job

Common Rules:

  1. Implement the appropriate interface (Api, GenericEventController, etc.)
  2. Can call Actions, Tasks, RepositoryTasks, and WorkflowActions
  3. Handle authorization at this layer with @PreAuthorize
  4. No business logic — delegate everything to Actions

REST Controller

@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));
    }

    @PreAuthorize("hasAnyRole('UsrUE', 'UsrUM')")
    @PutMapping("/{id}")
    public ResponseEntity<User> update(
            @PathVariable String id,
            @Validated @RequestBody UpdateUserRequest request) {
        return ResponseEntity.ok(call(UpdateUserAction.class, id, request));
    }
}

With Temporal Workflow — implement Workflow interface alongside Api:

@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));

        // Start async workflow — returns immediately
        callAsync(ProcessOrderWorkflowAction.class, ProcessOrderRequest.of(entity.getId()));

        // Return 202 Accepted for async processing
        return ResponseEntity
            .status(HttpStatus.ACCEPTED)
            .body(plainToClass(entity, Order.class));
    }
}

Controller Naming Convention:

Suffix Scope URL Prefix Example
UMController User Management /um/ UserUMController
SMController System Management (admin) /sm/ UserSMController
GMController Global Management (gRPC) UserGMController

gRPC Controller

@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();
    }
}

Event Handler

For processing cloud events (Pub/Sub, Kafka events):

@Component
public class UserEventHandler implements GenericEventController {

    @CloudFunctionEventHandler(topic = "user-created")
    public void onUserCreated(UserCreatedEvent event) {
        call(ProcessNewUserAction.class, event);
    }
}

Web Controller

For server-side rendered pages:

@Controller
public class UserWebController implements GenericController {

    @GetMapping("/users")
    public String listUsers(Model model) {
        model.addAttribute("users", call(FindAllUsersAction.class));
        return "users/list";
    }
}

Scheduled Job

Background jobs with distributed locking for cluster-safe execution:

@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 Convention:

Name Purpose
Process{Entity}Job Main processing loop
Stuck{Entity}Job Recovery for stuck IN_PROGRESS records

Temporal Workflows

Trellis provides first-class Temporal.io integration, mapping Temporal concepts to the Action/Task pattern:

Trellis Temporal Description
WorkflowAction Workflow Orchestrates the business process
WorkflowTask Activity Single atomic operation
WorkflowRepositoryTask Activity Database operation

WorkflowAction

WorkflowAction represents a durable Use Case that survives process restarts.

Interfaces: WorkflowAction (no args), WorkflowAction1<R, P> … up to WorkflowAction5

Annotations: @Workflow (required), @Async (optional, for non-blocking)

import net.trellisframework.workflow.temporal.action.WorkflowAction1;
import net.trellisframework.workflow.temporal.annotation.Async;
import net.trellisframework.workflow.temporal.annotation.Workflow;

@Workflow Annotation:

Attribute Default Description
taskQueue "" Temporal task queue name
executionTimeout "" Maximum workflow execution time (e.g., "2h")
runTimeout "" Maximum single run time
taskTimeout "" Maximum workflow task time
version "0.0.0" Workflow version for safe deployments

Example:

@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());  // Compensation
            return Order.failed(request.getRefId(), Messages.PAYMENT_FAILED);
        }

        // Step 4: Persist
        OrderEntity entity = call(SaveOrderTask.class, OrderEntity.of(request, payment));

        // Step 5: Async notification (fire-and-forget child workflow)
        Optional.ofNullable(request.getWebhookUrl())
            .filter(StringUtils::isNotBlank)
            .ifPresent(url -> callAsync(NotifyWebhookWorkflowAction.class,
                WebhookRequest.of(url, entity)));

        return plainToClass(entity, Order.class);
    }
}

@Async Annotation: When present, the API returns immediately with a workflow ID. The workflow continues executing in the background on the Temporal server.


WorkflowTask

WorkflowTask is a Temporal Activity — a single atomic operation.

Interfaces: WorkflowTask1<R, P> … up to WorkflowTask5

Annotation: @Activity (NOT @Service)

import net.trellisframework.workflow.task.WorkflowTask1;
import net.trellisframework.workflow.annotation.Activity;
import net.trellisframework.workflow.annotation.Retry;
import net.trellisframework.workflow.annotation.Backoff;

@Activity Annotation:

Attribute Default Description
startToCloseTimeout "60s" Max time from 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 strategy
include {} Exception types to retry on
exclude {} Exception types to NOT retry on

@Backoff Annotation:

Attribute Default Description
delay 1000 Initial delay in milliseconds
maxDelay 60000 Maximum delay in milliseconds
multiplier 2.0 Exponential backoff multiplier

Example:

@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();
    }
}

Example — External API call with heartbeat:

@Activity(
    retry = @Retry(maxAttempts = 5, backoff = @Backoff(delay = 2000, maxDelay = 60000, multiplier = 2.0)),
    startToCloseTimeout = "2m",
    heartbeat = "30s"
)
public class CallPaymentGatewayTask implements WorkflowTask1<PaymentResult, PaymentRequest> {

    @Override
    public PaymentResult execute(PaymentRequest request) {
        return paymentClient.process(request);
    }
}

WorkflowRepositoryTask

Temporal Activity with database access via a single repository.

Interface: WorkflowRepositoryTask1<Repository, ReturnType, InputType> (up to WorkflowRepositoryTask5)

Annotation: @Activity (NOT @Service)

Parameter Order: WorkflowRepositoryTask1<Repository, ReturnType, InputType> — Repository is always first.

@Activity
public class SaveOrderTask implements WorkflowRepositoryTask1<OrderRepository, OrderEntity, OrderEntity> {

    @Override
    public OrderEntity execute(OrderEntity entity) {
        return getRepository().save(entity);
    }
}

@Activity
public class FindOrderByIdTask implements WorkflowRepositoryTask1<OrderRepository, Optional<OrderEntity>, String> {

    @Override
    public Optional<OrderEntity> execute(String id) {
        return getRepository().findById(id);
    }
}

Workflow Helper Methods

Available inside WorkflowAction:

Method Description
call(Task.class, args) Execute activity synchronously
call(WorkflowAction.class, args) Execute child workflow synchronously
callAsync(Task.class, args) Execute activity asynchronously, returns Promise<T>
callAsync(WorkflowAction.class, args) Start child workflow asynchronously, returns Promise<T>
callAsync(WorkflowAction.class, args, ClosePolicy) Child workflow with close policy
await(Promise<T>) Wait for a single promise
await(List<Promise<T>>) Wait for all promises
sleep(Duration) Temporal-safe sleep (workflow pauses)
sleepMinutes(long) Sleep for N minutes
getWorkflowId() Get current workflow execution ID
getAttempt() Get current retry attempt number
version(changeId, maxVersion) Version-based branching for safe deployments

Parallel Execution

Fan-out / fan-in — parallel activities:

@Workflow
public class GetAllPricesWorkflowAction implements WorkflowAction1<List<Price>, List<String>> {

    @Override
    public List<Price> execute(List<String> airlines) {
        // Launch all activities in parallel
        List<Promise<Price>> promises = airlines.stream()
            .map(airline -> callAsync(GetAirlinePriceTask.class, airline))
            .toList();

        // Wait for all to complete
        return await(promises);
    }
}

Fire-and-forget (no await):

callAsync(SendNotificationWorkflowAction.class, notification);
// Continues immediately, doesn't wait for result

Async with deferred await:

Promise<Result> promise = callAsync(ChildWorkflowAction.class, input);
// ... do other work ...
Result result = await(promise);  // Wait when needed

WorkflowOption & ClosePolicy

WorkflowOption configures child workflow execution:

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 both
callAsync(ProcessEmailVerificationWorkflowAction.class, entity,
    WorkflowOption.of("process-" + entity.getId()));

ClosePolicy controls child workflow behavior when the parent closes:

Policy Description
TERMINATE Child terminates when parent closes
ABANDON Child continues running independently (default)
REQUEST_CANCEL Request cancellation of child

Validation

Trellis provides a dual validation system: annotations for static rules and FluentValidator for dynamic business rules.

Annotation-Based Validation

Use Jakarta/Trellis annotations directly on Payload fields for simple, static validation:

@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class AddUserRequest implements Payload {

    @Required
    @Email
    private String email;

    @Required
    @Length(min = 2, max = 100)
    private String name;

    @Phone
    private String phoneNumber;

    @Range(min = 18, max = 120)
    private Integer age;
}

FluentValidator

For dynamic rules that require database lookups, external API validation, or conditional logic, implement FluentValidator<T> on your Payload:

@Data
@NoArgsConstructor
@AllArgsConstructor(staticName = "of")
public class AddUserRequest implements Payload, FluentValidator<AddUserRequest> {

    @Required
    @Email
    private String email;

    @Required
    private String name;

    private String refId;
    private Customer customer;

    @Override
    public void execute() {
        // Dynamic rule: Check if email already exists (calls RepositoryTask)
        addRule(
            x -> call(ExistsUserByEmailTask.class, x.getEmail()),
            () -> new ConflictException(Messages.EMAIL_ALREADY_EXISTS)
        )
        // Dynamic rule: Validate API key (calls Task)
        .addRule(
            x -> !call(ValidateApiKeyTask.class, x.getApiKey()),
            () -> new UnauthorizedException(Messages.INVALID_API_KEY)
        )
        // Dynamic rule: Check quota (calls Action)
        .addRule(
            x -> !call(CheckQuotaAction.class, x.getCustomer().getId()),
            () -> new ForbiddenException(Messages.QUOTA_EXCEEDED)
        )
        // Auto-populate: Set default refId
        .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()
        )));
    }
}

addRule Patterns:

Pattern Behavior
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)

What FluentValidator Can Call:

Callable Allowed
Actions Yes
Tasks Yes
RepositoryTasks Yes
WorkflowTasks No
WorkflowActions No

Available Validation Annotations

The validator module provides these custom annotations:

Annotation Description
@Required Field must not be null or blank
@Email Valid email format
@Phone Valid phone number
@Length(min, max) String length constraints
@Size(min, max) Collection size constraints
@Range(min, max) Numeric range constraints
@Value(values) Must be one of specific values
@Regex(pattern) Must match regex pattern
@URL Valid URL format
@File(types) Valid file type
@NationalCode Valid national code (Luhn algorithm)
@BankCardNumber Valid bank card number (Luhn)
@IBan Valid IBAN
@ZipCode Valid zip code
@Age(min, max) Age range validation
@TimeZone Valid timezone identifier

Data Access

Trellis provides a unified repository abstraction across three database engines:

JPA / SQL (data-sql)

For relational databases (PostgreSQL, Oracle) with QueryDSL support.

Module: data-sql

Base Interface: GenericJpaRepository<TEntity, ID>

Features:

@Repository
public interface OrderRepository extends GenericJpaRepository<OrderEntity, String> {

    Optional<OrderEntity> findByRefId(String refId);

    default List<OrderEntity> findForProcessing(int limit) {
        QOrderEntity entity = QOrderEntity.orderEntity;
        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();
    }

    default long countByState(State state) {
        QOrderEntity entity = QOrderEntity.orderEntity;
        return getFactory().selectFrom(entity)
            .where(entity.state.eq(state))
            .fetchCount();
    }
}

MongoDB (data-mongo)

For document-oriented storage.

Module: data-mongo

Base Interface: GenericMongoRepository<TEntity>

@Repository
public interface ProductRepository extends GenericMongoRepository<ProductEntity> {
    // Spring Data MongoDB query methods
}

Elasticsearch (data-elastic)

For full-text search and analytics.

Module: data-elastic

Base Interface: GenericElasticRepository<TEntity>

Key Methods:

Method Description
search(query, pageable) Full-text search with pagination
findAll(filters) Find all with filters
findById(id) Find by document ID
findIds(query) Return IDs only
count(query) Count matching documents
aggregations(query) Run aggregation queries
msearch(queries) Multi-search in single request
save(entity) Index a document
update(id, doc) Update a document
delete(id) Delete a document
updateByQuery(query, script) Bulk update
deleteByQuery(query) Bulk delete

Features:

Enum Converters

Persist Java enums as database values using IEnumerated and GenericEnumConverter:

// 1. Enum implements IEnumerated<T>
@Getter
@AllArgsConstructor
public enum State implements IEnumerated<Integer> {
    PENDING(0),
    IN_PROGRESS(1),
    DONE(2),
    FAILED(3),
    CANCELLED(4);

    private final Integer db;
}

// 2. Converter extends GenericEnumConverter
public class Transformers {
    @Converter
    public static class StateToInteger extends GenericEnumConverter<State, Integer> {
        public StateToInteger() {
            super(State.class);
        }
    }
}

// 3. Use @Convert on entity field
@Column(name = "state")
@Convert(converter = Transformers.StateToInteger.class)
private State state = State.PENDING;

State Machine

Trellis entities commonly follow this state machine pattern:

PENDING ──→ IN_PROGRESS ──→ DONE
   │                    ├──→ FAILED
   │                    └──→ CANCELLED
   ▼
 QUEUED (for retry)
State Description
PENDING Waiting to be processed
QUEUED Temporarily delayed (retry backoff)
IN_PROGRESS Currently being processed
DONE Successfully completed
FAILED Failed after max retries
CANCELLED Cancelled by system or user

Pessimistic Locking

For concurrent record processing, use pessimistic locking with NO_WAIT:

.setLockMode(LockModeType.PESSIMISTIC_WRITE)
.setHint("jakarta.persistence.lock.timeout", -2)

Lock Timeout Values:

Value Behavior
-2 NO_WAIT — Immediate fail if locked (recommended)
-1 Wait indefinitely
n Wait n milliseconds

Caching

Redis Cache (cache-redis)

Module: cache-redis

Dependencies: Jedis 7.2, Redisson 3.43

Redis provides distributed caching with configurable TTL and serialization:

@CacheableConfig(value = "USER_CACHE", ttl = "1h", serializer = CacheSerializer.JSON)
@Cacheable(cacheNames = "USER_CACHE", key = "#email")
public User findByEmail(String email) { ... }

@CacheableConfig(value = "ORDER_CACHE", ttl = "30m", serializer = CacheSerializer.JSON)
@Cacheable(cacheNames = "ORDER_CACHE", key = "#id", unless = "#result == null")
public Order findById(String id) { ... }

@CacheEvict(cacheNames = "USER_CACHE", key = "#email")
public void updateUser(String email, UpdateUserRequest request) { ... }

Caffeine Cache (cache-caffeine)

Module: cache-caffeine

In-memory caching using Caffeine 3.2 with wildcard pattern support:

@CacheableConfig(value = "LOCAL_CACHE", ttl = "5m")
@Cacheable(cacheNames = "LOCAL_CACHE", key = "#key")
public String lookupValue(String key) { ... }

Cache Configuration

@CacheableConfig Annotation:

Attribute Default Description
value {} Cache names
ttl "" Time-to-live ("1h", "30m", "1d")
serializer JDK Serialization strategy

CacheSerializer Options:

Serializer Description
JDK Java serialization (default)
BYTE_ARRAY Raw byte array
STRING String serialization
JSON Jackson JSON serialization (recommended)

Distributed Locking

Module: cache-redis

Redis-backed distributed locks using Redisson for cluster-safe operations:

@DistributedLock(value = "PROCESS_ORDERS", skipIfLocked = true, cooldown = "20s")
@Scheduled(fixedDelay = 1000)
public void processOrders() { ... }

@DistributedLock Annotation:

Attribute Default Description
value Lock resource name (required)
key "" Dynamic key using SpEL expressions
fairLock false Use fair lock (FIFO ordering)
waitTime "60s" Maximum time to wait for lock
leaseTime "60s" How long to hold the lock
skipIfLocked false Skip execution if lock is held
cooldown "" Cooldown period after lock release

Rate Limiting

Module: cache-redis

Advanced rate limiting with multiple time windows, sliding windows, and concurrent permits:

RateLimit rateLimit = RateLimit.builder()
    .maxPerSecond(10)
    .maxPerMinute(100)
    .maxPerHour(1000)
    .maxPerDay(10000)
    .maxConcurrent(5)
    .permitTimeout("30s")
    .coolOff("5m")
    .build();

Components:

Class Description
RateLimit Rate limit configuration builder
AdvancedRateLimiter Rate limiting algorithm implementation
RateLimitResource Tracks rate limit state per resource
ResourceState Current resource state
RedisCounter Atomic counter backed by Redis
RedisSlidingWindowCounter Sliding window counter
RedisSemaphore Distributed semaphore
ExpirableSemaphore Semaphore with automatic expiration

Messaging & Streaming

Trellis provides message streaming through Spring Cloud Stream with implementations for Kafka and RabbitMQ.

Kafka (stream-kafka)

Module: stream-kafka

// Producer — send messages
@Service
public class OrderEventProducer {

    @Autowired
    private KafkaMessageProducer producer;

    public void publishOrderCreated(Order order) {
        producer.send(ProducerMessage.of("order-created", order));
    }
}

// Consumer — receive messages
@Component
public class OrderEventHandler implements GenericEventController {

    @CloudFunctionEventHandler(topic = "order-created")
    public void onOrderCreated(OrderCreatedEvent event) {
        call(ProcessNewOrderAction.class, event);
    }
}

RabbitMQ (stream-rabbit)

Module: stream-rabbit

@Service
public class NotificationProducer {

    @Autowired
    private RabbitMessageProducer producer;

    public void sendNotification(Notification notification) {
        producer.send(ProducerMessage.of("notifications", notification));
    }
}

Core Abstractions (stream-core):

Interface Description
MessageProducer Common producer interface with send(ProducerMessage)
ProducerMessage<T> Message payload wrapper with topic/routing info
StreamFunctionEvent Stream function event
StreamConsumerEvent Consumer event handler

Security & OAuth

Keycloak Integration

Module: oauth-resource-keycloak

Trellis integrates with Keycloak for OAuth2 resource server authentication with JWT token validation.

Features:

Accessing the Current User:

// Get current user's ID (from JWT 'sub' claim)
String userId = OAuthSecurityContext.getPrincipalId();

// Get current user's email
String email = OAuthSecurityContext.getEmail();

// Get full principal with permissions
Principle principal = OAuthSecurityContext.getPrinciple();

// Decode a specific access token
Principle principal = OAuthSecurityContext.findPrincipleByAccessToken(token);

Standard Keycloak Claims (KeycloakClaimNames):

Claim Description
SUB User ID
EMAIL User email
PREFERRED_USERNAME Username
GIVEN_NAME First name
FAMILY_NAME Last name

Role-Based Access Control

Use @PreAuthorize on controllers with the Trellis role naming convention:

Role Format: {Domain}{Scope}{Permission}

Component Values
Domain Usr (User), Ord (Order), Ver (Verification), etc.
Scope U (User), S (System), G (Global)
Permission R (Read), A (Add), E (Edit), D (Delete), M (Manage/All)

Examples:

Role Meaning
UsrUR User-scope User Read
UsrUM User-scope User Manage (all permissions)
OrdSA System-scope Order Add
VerUA User-scope Verification Add
@PreAuthorize("hasAnyRole('UsrUR', 'UsrUM')")
@GetMapping("/{id}")
public ResponseEntity<User> read(@PathVariable String id) { ... }

@PreAuthorize("hasAnyRole('UsrUA', 'UsrUM')")
@PostMapping
public ResponseEntity<User> add(@RequestBody AddUserRequest request) { ... }

Multi-Tenancy

The oauth-resource-keycloak module supports multi-tenancy via issuer-based resolution:


HTTP Client

Module: http

Trellis provides an HTTP client built on Retrofit 3.0 and OkHttp with built-in retry logic.

Step 1 — Define the client interface:

public interface UserServiceClient {

    static UserServiceClient getInstance() {
        return HttpHelper.getHttpInstance(
            Config.USER_SERVICE_URL.getOrDefult("https://api.example.com"),
            UserServiceClient.class,
            10,    // connectTimeout (seconds)
            10,    // readTimeout (seconds)
            60     // writeTimeout (seconds)
        );
    }

    @GET("/api/users/{id}")
    @Headers({"Content-Type: application/json"})
    Call<User> read(@Header("Authorization") String token, @Path("id") String id);

    @POST("/api/users")
    @Headers({"Content-Type: application/json"})
    Call<User> add(@Header("Authorization") String token, @Body AddUserRequest request);

    @GET("/api/users")
    @Headers({"Content-Type: application/json"})
    Call<List<User>> browse(@Header("Authorization") String token, @Query("page") int page);
}

Step 2 — Use in an Action:

@Service
public class ReadExternalUserAction implements Action1<User, String> {

    @Override
    public User execute(String userId) {
        String token = "Bearer " + OAuthSecurityContext.getAccessToken();
        return HttpHelper.execute(UserServiceClient.getInstance().read(token, userId));
    }
}

HttpHelper.getHttpInstance Parameters:

Parameter Description
baseUrl Base URL of the target API
clientClass Retrofit interface class
connectTimeout Connection timeout in seconds
readTimeout Read timeout in seconds
writeTimeout Write timeout in seconds

Features:

Exception Hierarchy:

The http module defines a complete set of HTTP exceptions:

Exception Status Description
BadRequestException 400 Invalid input, validation failures
UnauthorizedException 401 Authentication required
ForbiddenException 403 Access denied
NotFoundException 404 Resource not found
ConflictException 409 Duplicate resource
PayloadTooLargeException 413 Request body too large
TooManyRequestsException 429 Rate limit exceeded
InternalServerException 500 Server error
BadGatewayException 502 Bad gateway
ServiceUnavailableException 503 Service unavailable
GatewayTimeoutException 504 Gateway timeout

Email & Messaging

Module: message

Send emails via SMTP or Mailgun API:

SMTP:

SendMailRequest request = SendMailRequest.of(
    "[email protected]",
    "Subject",
    "Email body content"
);
call(SendMailAction.class, request);

Mailgun:

SendMailByMailgunRequest request = SendMailByMailgunRequest.of(
    "[email protected]",
    "Subject",
    MailgunTemplate.of("template-name", Map.of("key", "value"))
);
call(SendMailByMailgunAction.class, request);

Available Actions:

Action Description
SendMailAction Send email via SMTP
SendMailWithConfigurationAction Send with custom mail server configuration
SendMailByMailgunAction Send via Mailgun API

Response Status (SendMessageStatus): SENT, FAILED, etc.


WebSocket

Module: socket-websocket

Real-time bidirectional communication with OAuth2 token authentication:

Configuration Properties (WebSocketProperties):

Property Description
Broker definitions Message broker configuration (topics, prefixes)
Interceptor definitions Channel interceptors for auth/logging
Message buffer sizes Send/receive buffer limits
Thread pool settings Thread pool for message handling
Endpoint patterns WebSocket endpoint URLs
Allowed origins CORS origin patterns

Features:


Utilities

Module: util

A comprehensive utility library covering AWS, cryptography, export, and more.

AWS S3 Client

Pre-signed URL generation, file upload/download, and object management:

@Autowired
private AwsS3Client s3Client;

// Generate pre-signed upload URL (AWS SigV4)
String uploadUrl = s3Client.generatePresignedUploadUrl(
    "bucket", "path/file.pdf", Duration.ofMinutes(15));

// Generate pre-signed download URL
String downloadUrl = s3Client.generatePresignedDownloadUrl(
    "bucket", "path/file.pdf", Duration.ofMinutes(60));

// Generate public URL
String publicUrl = s3Client.generatePublicUrl("bucket", "path/file.pdf");

// Upload file
s3Client.upload("bucket", "path/file.pdf", inputStream);

// Download file
InputStream file = s3Client.download("bucket", "path/file.pdf");

// List objects
List<S3Object> objects = s3Client.browse("bucket", "path/");

Configuration (AwsS3ClientProperties): Region, access key, secret key, endpoint.

Cryptography

// Hashing
String sha256 = CryptoUtil.sha256("data");
String md5 = CryptoUtil.md5("data");
String hmac = CryptoUtil.hmacSha256("data", "secret");

// AES Encryption/Decryption
String encrypted = CryptoUtil.aesEncrypt("plaintext", "secretKey");
String decrypted = CryptoUtil.aesDecrypt(encrypted, "secretKey");

JWT Tokens

Build and sign JWT tokens with HMAC algorithms:

String token = Jwt.builder()
    .subject("user-123")
    .claim("email", "[email protected]")
    .claim("roles", List.of("admin"))
    .expiration(Duration.ofHours(1))
    .signWith(Jwt.Algorithm.HS256, "your-secret-key")
    .build();

Supported Algorithms: HS256, HS384, HS512

Export (Excel / CSV)

Export data to multiple formats:

// Export to Excel (.xlsx)
byte[] excel = ExportUtil.toExcel(dataList, headers);

// Export to CSV
byte[] csv = ExportUtil.toCsv(dataList, headers, ",");

// Export to TSV
byte[] tsv = ExportUtil.toTsv(dataList, headers);

// Convert Excel to CSV
byte[] csv = ExportUtil.excelToCsv(excelBytes);

// Convert CSV to Excel
byte[] excel = ExportUtil.csvToExcel(csvBytes);

Supports: List<Map<String, Object>> and POJO objects with automatic field extraction.

Load Balancing

Client-side load balancing strategies:

// Random selection
LoadBalancer<Server> random = new RandomLoadBalancer<>(servers);
Server server = random.next();

// Round Robin
LoadBalancer<Server> roundRobin = new RoundRobinLoadBalancer<>(servers);

// Weighted Random
LoadBalancer<Server> weighted = new WeightRandomLoadBalancer<>(weightedServers);

// Weighted Round Robin
LoadBalancer<Server> weightedRR = new WeightRoundRobinLoadBalancer<>(weightedServers);

Other Utilities

Utility Description
StringUtil Hex encoding/decoding, UUID generation, UTF-8 conversion
TextUtil Text processing and manipulation
URLUtil URL parsing and encoding
JsonUtil Jackson JSON serialization/deserialization
FileUtil File operations
DateUtil Date utilities
JalaliCalendar Persian (Shamsi) calendar support
DurationParser Parse duration strings ("60s", "5m", "2h", "1d")
NetworkUtil Network connectivity utilities
IpUtil IP address utilities
PhoneUtil Phone number parsing and validation
NumberUtil Number utilities
ObjectUtil Object comparison and manipulation
ReflectionUtil Reflection-based property access
EnvironmentUtil Environment variable access
Threads Thread utilities
ModelMapper JSON-based object mapping interface

Configuration

Each module uses the IConfig pattern for centralized, type-safe configuration:

@Getter
@AllArgsConstructor
public enum Config implements IConfig {
    MAX_RETRY("user.max-retry", "3"),
    BATCH_SIZE("user.batch-size", "100"),
    TIMEOUT("user.timeout", "30"),
    CACHE_TTL("user.cache-ttl", "24");

    private final String property;
    private final String defaultValue;

    @Override
    public String getProperty() {
        return property;
    }

    @Override
    public String getDefaultValue() {
        return defaultValue;
    }
}

// Usage — values come from Spring Config Server or application.yml
int maxRetry = Integer.parseInt(Config.MAX_RETRY.get());
int batchSize = Integer.parseInt(Config.BATCH_SIZE.get());

Config values are resolved from Spring’s property sources (application.yml, Config Server, environment variables) with fallback to the default value.


Error Handling

Exception Classes

Always use Trellis HTTP exceptions. Never create custom exception classes.

Exception HTTP Status Usage
BadRequestException 400 Validation errors, invalid input
UnauthorizedException 401 Authentication required
ForbiddenException 403 Access denied, insufficient permissions
NotFoundException 404 Resource not found
ConflictException 409 Duplicate resource, already exists
PayloadTooLargeException 413 Request body too large
TooManyRequestsException 429 Rate limit exceeded
InternalServerException 500 Server errors

Messages Pattern

Never hardcode exception messages. Create a Messages enum implementing MessageHandler:

public enum Messages implements MessageHandler {
    USER_NOT_FOUND,
    EMAIL_ALREADY_EXISTS,
    EMAIL_IS_REQUIRED,
    INVALID_EMAIL_FORMAT,
    FORBIDDEN_ACCESS,
    MAX_RETRY_EXCEEDED,
    TIMEOUT_EXCEEDED
}

Usage in Actions:

return call(FindUserByIdTask.class, id)
    .orElseThrow(() -> new NotFoundException(Messages.USER_NOT_FOUND));

Usage in FluentValidator:

addRule(
    x -> StringUtils.isBlank(x.getEmail()),
    () -> new BadRequestException(Messages.EMAIL_IS_REQUIRED)
);

WRONG — never hardcode messages:

throw new NotFoundException("User not found");  // NEVER DO THIS

Logging

Trellis provides a custom Logger with performance monitoring support:

// Standard logging
Logger.info("User created: {}", user.getId());
Logger.error("Failed to process user", exception);

// Performance logging — only logs if operation exceeds threshold
Logger.info(
    () -> heavyOperation(),                          // Operation to execute
    (time, result) -> time > 1000,                   // Condition: log if > 1s
    (time, result) -> String.format("Operation took %d ms", time)  // Message
);

The LogAspect provides AOP-based logging for intercepting method calls automatically.


Concurrency

Thread Pools

// Create dedicated thread pool per domain
public static final ExecutorService PROCESSING_POOL =
    Executors.newFixedThreadPool(Integer.parseInt(Config.BATCH_SIZE.get()));

Parallel Processing

List<CompletableFuture<Void>> futures = entities.stream()
    .map(entity -> CompletableFuture.runAsync(
        () -> call(ProcessEntityAction.class, entity),
        PROCESSING_POOL
    ))
    .toList();
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();

Stuck Record Recovery

Implement recovery jobs for records stuck in IN_PROGRESS state:

@Component
public class StuckOrderJob extends Job {

    @DistributedLock(value = "STUCK_ORDER_JOB", skipIfLocked = true, cooldown = "60s")
    @Scheduled(fixedDelay = 60000)
    public void execute() {
        try {
            call(ResetStuckOrdersAction.class);
        } catch (Throwable t) {
            Logger.error("StuckOrderJob", t.getMessage());
        }
    }
}

Code Style

Rule Convention
Boilerplate Use Lombok (@Data, @Getter, @Setter, etc.)
Nullable returns Use Optional — never return null
Factory methods @AllArgsConstructor(staticName = "of")
Null-safe chains Use Optional chaining
Nesting Prefer early returns to reduce nesting
Comments No comments in code — code should be self-explanatory
// Optional chaining
Optional.ofNullable(entity.getStatus())
    .filter(Status::isActive)
    .ifPresent(x -> processActive(entity));

// Early return
if (entity == null) {
    return Optional.empty();
}

// Factory method
UserEntity entity = UserEntity.of(request.getEmail(), request.getName());

Deployment

Environment Variables

Variable Default Description
SERVER_PORT 80 Application API port
ACTUATOR_PORT 8080 Management/health check port
APPLICATION_MODE production Spring profile
CONFIG_SERVER_URI http://config Spring Cloud Config Server URI

Docker Build

Trellis uses the JIB Maven Plugin for containerized builds:

mvn package -P production

Health Checks


Module Reference

Module Artifact Description Key Dependencies
core core Foundation: logging, constants, Payload, MessageHandler Guava, Lombok, Jakarta Validation
context context Action/Task framework, FluentValidator, Job, execution contexts core, util
http http HTTP client (Retrofit 3.0), exception hierarchy core, Retrofit, OkHttp
util util AWS S3, Crypto, JWT, Export, LoadBalancer, Date/Phone utils core, http, AWS SDK v2, POI, PDFBox
validator validator 16+ validation annotations (@Email, @Phone, @Required, etc.) Hibernate Validator, util
data-core data-core Repository abstraction, QueryDSL predicates, search criteria validator, context, QueryDSL
data-sql data-sql JPA/PostgreSQL with GenericJpaRepository, QueryDSL integration data-core, Spring Data JPA, QueryDSL JPA
data-mongo data-mongo MongoDB with GenericMongoRepository data-core, Spring Data MongoDB
data-elastic data-elastic Elasticsearch 8.19 with full-text search, aggregations util, Elasticsearch Java Client
cache-core cache-core Cache abstraction, @CacheableConfig, CacheSerializer util, Spring Boot Cache
cache-caffeine cache-caffeine In-memory caching with wildcard patterns cache-core, Caffeine 3.2
cache-redis cache-redis Redis cache, @DistributedLock, rate limiting, semaphores cache-caffeine, Jedis 7.2, Redisson 3.43
stream-core stream-core Messaging abstraction, MessageProducer, ProducerMessage context, Spring Cloud Stream
stream-kafka stream-kafka Apache Kafka producer/consumer stream-core, Spring Cloud Stream Kafka
stream-rabbit stream-rabbit RabbitMQ producer/consumer stream-core, Spring Cloud Stream RabbitMQ
oauth-resource-keycloak oauth-resource-keycloak OAuth2 resource server, Keycloak, multi-tenancy context, Spring Security OAuth2
workflow-temporal workflow-temporal Temporal.io workflows, @Workflow, @Activity, @Retry core, context, Temporal SDK 1.32
message message Email via SMTP and Mailgun API context, Spring Mail, Mailgun API
socket-websocket socket-websocket WebSocket with OAuth2 authentication Spring WebSocket

Technology Stack

Component Technology Version
Language Java 21
Framework Spring Boot 4.0.1
Cloud Spring Cloud 2025.1.0
Build Maven 3.9+
ORM JPA + Hibernate + QueryDSL 5.1.0
Database PostgreSQL, Oracle
Document Store MongoDB
Search Engine Elasticsearch 8.19
Cache Redis (Jedis + Redisson) 7.2 / 3.43
In-Memory Cache Caffeine 3.2.3
Workflow Temporal.io 1.32.1
Messaging Apache Kafka, RabbitMQ
HTTP Client Retrofit + OkHttp 3.0.0
Security Spring Security + Keycloak 20.0.1
Serialization Jackson 3.0.3
Crypto Bouncycastle 1.83
AWS AWS SDK v2 (S3)
Export Apache POI (Excel), PDFBox 3.0.6
Email Spring Mail, Mailgun API, Simple Java Mail
Monitoring Micrometer, Spring Actuator 1.16.1
Container Docker (JIB Maven Plugin)

License

MIT License

Contributing

Contributions are welcome! Please read our contributing guidelines.

Support