OP

opik-backend

Apply consistent patterns for Java development in Opik, ensuring structured layers and correct naming conventions.

Install

mkdir -p .claude/skills/opik-backend && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/4502" && unzip -o skill.zip -d .claude/skills/opik-backend && rm skill.zip

Installs to .claude/skills/opik-backend

Activation

This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.

Java backend patterns for Opik. Use when working in apps/opik-backend, designing APIs, database operations, or services.
120 chars✓ has a “when” trigger
Intermediate

Key capabilities

  • Enforce layered service architecture (Resource/Service/DAO)
  • Apply Lombok @Builder and @NonNull conventions
  • Standardize plural/singular naming across code layers
  • Configure Jakarta validation for DTOs

How it works

Applies a strict set of architectural rules and design patterns derived from the Opik backend codebase.

Inputs & outputs

You give it
Code snippet or class name
You get back
Refactored code snippet adhering to Opik style guides

When to use opik-backend

  • Implement new API resources following layered architecture
  • Apply naming conventions to DAO and service classes
  • Configure Java records with proper Lombok builders

About this skill

Opik Backend

Architecture

  • Layered: Resource → Service → DAO (never skip layers)
  • DI: Guice modules, constructor injection with @Inject
  • Databases: MySQL (metadata, transactional) + ClickHouse (analytics, append-only)

Naming Conventions

Plural Names (Resources, Tests, URLs, DB Tables)

  • Resource classes: TracesResource, SpansResource, DatasetsResource (not TraceResource)
  • Resource test classes: TracesResourceTest, SpansResourceTest, DatasetsResourceTest (not TraceResourceTest)
  • URL paths: /v1/private/traces, /v1/private/spans (not /v1/private/trace)
  • DB table names: traces, spans, feedback_scores (not trace, span, feedback_score)

Singular Names (DAO, Service)

  • DAO classes: TraceDAO, SpanDAO, DatasetDAO (not TracesDAO)
  • Service classes: TraceService, SpanService, DatasetService (not TracesService)
// ✅ GOOD
@Path("/v1/private/traces")
public class TracesResource { }

// ✅ GOOD - DAO and Service use singular
public class TraceDAO { }
public class TraceService { }

// ✅ GOOD - test classes match plural resource name
public class TracesResourceTest { }

// ❌ BAD - singular test class
public class TraceResourceTest { }

// ❌ BAD - singular resource/URL
@Path("/v1/private/trace")
public class TraceResource { }

// ❌ BAD - plural DAO/Service
public class TracesDAO { }
public class TracesService { }

Lombok Conventions

Records and DTOs

  • Always annotate records/DTOs with @Builder(toBuilder = true)
  • Use builders (not constructors) when instantiating records
  • For internal records (built programmatically, never validated by Bean Validation), use Lombok @NonNull on required fields — it generates a runtime null check at construction
  • For request-body DTOs validated via @Valid cascade (Jakarta validators like @NotNull/@NotBlank/@Size), use Jakarta annotations only — do not stack @NonNull on top. Bean Validation already enforces the contract at the API boundary; doubling up is redundant noise
// ✅ GOOD - internal record, Lombok @NonNull
@Builder(toBuilder = true)
record MyData(@NonNull UUID id, @NonNull String name, String description) {}

MyData data = MyData.builder()
        .id(id)
        .name(name)
        .build();

// ✅ GOOD - request-body DTO, Jakarta validators only
@Builder(toBuilder = true)
public record MyRequest(
        @NotNull UUID id,
        @NotBlank String name,
        @NotNull @Size(min = 1, max = 1000) @Valid List<MyItem> items) {}

// ❌ BAD - plain constructor (positional mistakes, less readable)
new MyData(id, name, null);

// ❌ BAD - @Builder without toBuilder
@Builder
record MyData(UUID id, String name) {}

// ❌ BAD - stacking @NonNull and @NotNull on the same field
public record MyRequest(@NonNull @NotNull UUID id) {}

Dependency Injection

  • Use @RequiredArgsConstructor(onConstructor_ = @Inject) instead of manual constructors
// ✅ GOOD
@RequiredArgsConstructor(onConstructor_ = @Inject)
public class MyService {
    private final @NonNull DependencyA depA;
    private final @NonNull DependencyB depB;
}

// ❌ BAD - boilerplate constructor
public class MyService {
    private final DependencyA depA;
    @Inject
    public MyService(DependencyA depA) {
        this.depA = depA;
    }
}

Interfaces

  • Don't put validation annotations (@NonNull) on interface method parameters
  • Keep interfaces free of implementation details
// ✅ GOOD
interface MyService {
    void process(String workspaceId, UUID promptId);
}

// ❌ BAD - validation on interface
interface MyService {
    void process(@NonNull String workspaceId, @NonNull UUID promptId);
}

Critical Gotchas

StringTemplate Memory Leak

// ✅ GOOD
var template = TemplateUtils.newST(QUERY);

// ❌ BAD - causes memory leak via STGroup singleton
var template = new ST(QUERY);

List Access

// ✅ GOOD
users.getFirst()
users.getLast()

// ❌ BAD
users.get(0)
users.get(users.size() - 1)

SQL Query Construction

Never build a query out of Java string operations. No +, no String.format / .formatted(...), no StringBuilder, no MessageFormat, no String.join over clauses. A query is declared once as a text block, and everything that varies goes through exactly one of two mechanisms:

What variesMechanism
A value — id, name, timestamp, list of ids:placeholder + .bind("placeholder", value)
A fragment — predicate, sort clause, projected column, CTEStringTemplate <if(x)>…<endif>, <else>, <x> + template.add("x", …)

Why: interpolating values is the SQL-injection surface, and interpolating fragments hides which query a DAO actually runs — the declaration site stops being readable, and callers drift apart over time.

// ✅ GOOD - text block, values bound, structure via StringTemplate
@SqlQuery("""
        SELECT * FROM datasets
        WHERE workspace_id = :workspace_id
        <if(name)> AND name like concat('%', :name, '%') <endif>
        """)

// ❌ BAD - string concatenation
@SqlQuery("SELECT * FROM datasets " +
        "WHERE workspace_id = :workspace_id " +
        "<if(name)> AND name like concat('%', :name, '%') <endif> ")

A predicate that differs between callers is a fragment, so it belongs in the template — not in a %s slot the caller fills in:

// ❌ BAD - caller splices the predicate in
private static final String TOKEN_USAGE_NAMES_TEMPLATE = """
        SELECT DISTINCT name FROM (
            SELECT usage FROM spans FINAL
            WHERE workspace_id = :workspace_id
            AND %s
        ) ...
        """;

static String tokenUsageNames(String projectPredicate) {
    return TOKEN_USAGE_NAMES_TEMPLATE.formatted(projectPredicate);
}

// caller: tokenUsageNames("project_id IN :project_ids")

// ✅ GOOD - both shapes live in the template, the caller picks one
private static final String TOKEN_USAGE_NAMES = """
        SELECT DISTINCT name FROM (
            SELECT usage FROM spans FINAL
            WHERE workspace_id = :workspace_id
            <if(project_ids)> AND project_id IN :project_ids <endif>
            <if(project_id)> AND project_id = :project_id <endif>
        ) ...
        """;

var template = TemplateUtils.newST(TOKEN_USAGE_NAMES);
template.add("project_ids", true);
...
statement.bind("project_ids", projectIds.toArray(new UUID[0]));

A fragment that genuinely can't be enumerated in the template — a user-chosen sort field or filter clause — must be produced by the allow-listed builders (SortingQueryBuilder, FilterQueryBuilder), never assembled from raw request strings.

.formatted(...) stays correct for log and exception messages. The rule is about SQL text only.

Some %s query templates predate this rule. Don't copy them and don't add new ones.

Immutable Collections

// ✅ GOOD
Set.of("A", "B", "C")
List.of(1, 2, 3)
Map.of("key", "value")

// ❌ BAD
Arrays.asList("A", "B", "C")

API Design

  • Query parameters that accept lists: Use plural names from the start (e.g., exclude_category_names not exclude_category_name). Starting with a singular name and later adding a plural variant results in two redundant query params on the same endpoint. Plural names are backward-compatible since they work for both single and multiple values.

Error Handling

Use Jakarta Exceptions

throw new BadRequestException("Invalid input");
throw new NotFoundException("User not found: '%s'".formatted(id));
throw new ConflictException("Already exists");
throw new InternalServerErrorException("System error", cause);

Error Response Classes

  • Simple: io.dropwizard.jersey.errors.ErrorMessage
  • Complex: com.comet.opik.api.error.ErrorMessage
  • Never create new error message classes

Logging

Format Convention

// ✅ GOOD - values in single quotes
log.info("Created user: '{}'", userId);
log.error("Failed for workspace: '{}'", workspaceId, exception);

// ❌ BAD - no quotes
log.info("Created user: {}", userId);

Never Log

  • Emails, passwords, tokens, API keys
  • PII, personal identifiers
  • Database credentials

Reference Files

When not to use it

  • Working on non-Java projects
  • Implementing architecture that violates the layered pattern

Limitations

  • Strict layer separation is mandatory
  • Does not accommodate legacy non-layered patterns

How it compares

Enforces domain-specific naming and structural conventions beyond standard Java style guides.

Compared to similar skills

opik-backend side by side with the closest alternatives in the catalog.

SkillInstallsUpdatedSafetyDifficulty
opik-backend (this skill)12moNo flagsIntermediate
workflow-orchestration-patterns102moNo flagsAdvanced
java-pro344moNo flagsAdvanced
springboot-patterns115moNo flagsIntermediate

Try saying

Example prompts that trigger this skill in your AI assistant.

You might also like

workflow-orchestration-patterns

wshobson

Design durable workflows with Temporal for distributed systems. Covers workflow vs activity separation, saga patterns, state management, and determinism constraints. Use when building long-running processes, distributed transactions, or microservice orchestration.

10117

java-pro

sickn33

Master Java 21+ with modern features like virtual threads, pattern matching, and Spring Boot 3.x. Expert in the latest Java ecosystem including GraalVM, Project Loom, and cloud-native patterns. Use PROACTIVELY for Java development, microservices architecture, or performance optimization.

3492

springboot-patterns

affaan-m

Spring Boot 架构模式、REST API 设计、分层服务、数据访问、缓存、异步处理和日志记录。适用于 Java Spring Boot 后端工作。

1147

backend-microservice-development

TencentBlueKing

后端微服务开发规范,涵盖目录结构、分层架构(API/Service/DAO)、依赖注入、配置管理、Spring Boot 最佳实践。当用户进行后端开发、创建新微服务、编写 Kotlin/Java 代码或设计服务架构时使用。

213

microservice-infrastructure

TencentBlueKing

微服务基础设施指南,涵盖条件配置、事件驱动架构、服务间通信、国际化与日志等微服务架构的核心基础设施。当用户实现服务间调用、配置多环境、实现异步通信、处理国际化或规范日志输出时使用。

411

common-technical-practices

TencentBlueKing

通用技术实践指南,涵盖 AOP 切面、分布式锁、重试机制、参数校验、性能监控、定时任务、审计日志等后端开发中的常见技术实践。当用户需要实现横切关注点、处理并发控制、配置重试策略、添加性能监控或实现审计功能时使用。

26

Search skills

Search the agent skills registry