Skip to content

Coding Guide

Alex Kuleshov edited this page Feb 18, 2026 · 5 revisions

Coding Guide

Code style, conventions, and best practices for the GolemCore Bot codebase.


Commit Messages

This project follows Conventional Commits 1.0.0.

Format

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

Types

Type When to use
feat New feature or capability
fix Bug fix
refactor Code change that neither fixes a bug nor adds a feature
test Adding or updating tests
docs Documentation only
chore Build config, CI, dependencies, tooling
perf Performance improvement
style Formatting, whitespace (no logic change)

Scope (optional)

Use the module or area name: llm, telegram, tools, skills, mcp, auto, routing, security, storage, loop.

Examples

feat(tools): add BrowserTool screenshot mode

fix(llm): handle empty response from Anthropic API

refactor(routing): extract MessageContextAggregator from SkillRoutingSystem

test(mcp): add McpClient lifecycle tests

chore: upgrade langchain4j to 1.11.0

feat(skills)!: rename nextSkill field to next_skill in YAML frontmatter

BREAKING CHANGE: skill YAML files must use next_skill instead of nextSkill.

Rules

  • Use imperative mood: "add feature", not "added feature" or "adds feature"
  • First line under 72 characters
  • No period at the end of the subject line
  • Breaking changes: append ! after type/scope and add a BREAKING CHANGE: footer

Java Style

Explicit Type Declarations

Always declare variable types explicitly. Do not use var.

// Correct
List<Skill> available = getAvailableSkills();
String sessionId = buildSessionId(channelType, chatId);
Map<String, Skill> registry = new ConcurrentHashMap<>();

// Incorrect
var available = getAvailableSkills();

Constructor Injection

All Spring-managed beans use constructor injection via Lombok's @RequiredArgsConstructor. Field injection (@Autowired) is prohibited.

@Service
@RequiredArgsConstructor
@Slf4j
public class SessionService implements SessionPort {
    private final StoragePort storagePort;
    private final ObjectMapper objectMapper;
    private final Clock clock;
}

@Lazy is prohibited. Break circular dependencies by:

  1. Extracting a shared interface/service
  2. Using ApplicationEventPublisher for one-way notifications
  3. Moving the dependency into a method parameter

Class Organization

@Service
@RequiredArgsConstructor
@Slf4j
public class ExampleService {
    // 1. Static constants
    private static final String DIR_NAME = "examples";

    // 2. Injected dependencies (private final)
    private final StoragePort storagePort;

    // 3. Mutable state (caches, registries)
    private final Map<String, Item> cache = new ConcurrentHashMap<>();

    // 4. @PostConstruct
    // 5. Public interface methods (@Override)
    // 6. Public methods
    // 7. Private methods
}

Naming Conventions

Classes:

Suffix Layer Example
*Service Domain services SessionService
*System Pipeline systems ToolLoopExecutionSystem
*Tool Tool implementations FileSystemTool
*Adapter Outbound adapters Langchain4jAdapter
*Port Port interfaces LlmPort, StoragePort
*Component Component interfaces ToolComponent

Methods:

Pattern Purpose Example
get* Retrieve, throw if missing getSession()
find* Lookup, return Optional findByName()
is*, has* Boolean query isEnabled(), hasMcp()
create*, build* Factory createSession()
process Pipeline processing system.process(context)
execute Run an action tool.execute(params)

Imports

No wildcard imports. Static imports allowed in tests only.

// Correct
import java.util.List;
import java.util.Map;

// Avoid
import java.util.*;

Lombok

Annotation Where Purpose
@RequiredArgsConstructor Services, adapters, systems, tools Constructor injection
@Slf4j Any class that logs Generates log field
@Data Domain model POJOs Getters, setters, equals, hashCode, toString
@Builder Domain models, request/response objects Builder pattern
@NoArgsConstructor Models deserialized by Jackson Required for JSON/YAML parsing
@AllArgsConstructor Models with @NoArgsConstructor Complete constructor

Gotcha

Computed getters in @Data classes get serialized by Jackson. Mark them @JsonIgnore:

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Goal {
    private String id;

    @Builder.Default
    private List<AutoTask> tasks = new ArrayList<>();

    @JsonIgnore
    public long getCompletedTaskCount() {
        return tasks.stream()
                .filter(t -> t.getStatus() == TaskStatus.COMPLETED)
                .count();
    }
}

Spring Patterns

Bean Design

All beans always exist at runtime. Use isEnabled() for runtime enable/disable — never @ConditionalOnProperty.

Stereotypes

  • @Service — domain services
  • @Component — adapters, tools, infrastructure
  • @Configuration — config classes; @Bean methods with injected fields must be static

Logging

Use @Slf4j. Parametrized messages only — no string concatenation.

Level Use for
error Failures that need attention (with exception)
warn Recoverable issues (rate limit, fallback)
info Milestones (session created, skill matched)
debug Internal flow (timing, cache hits)
trace Very detailed (raw content)

Use [Area] prefix:

log.info("[AutoMode] Created goal '{}'", title);
log.debug("[MCP] Starting server for skill: {}", skillName);

Error Handling

  • No custom exception hierarchy — use IllegalStateException, IllegalArgumentException
  • Use Optional for lookups, never return null from public methods
  • Catch broadly in I/O layers with // NOSONAR comment
  • Log at appropriate level: debug for expected failures, error for unexpected

Testing

  • Test class: *Test suffix
  • Method: shouldDoSomethingWhenCondition() — no test prefix
  • Pattern: Arrange-Act-Assert
  • Mocking: Mockito, create mocks in @BeforeEach
  • Varargs mock: use custom Answer on mock creation
  • Use @ParameterizedTest + @ValueSource for input validation
@Test
void shouldRejectPathTraversalAttempt() {
    // Arrange
    Map<String, Object> params = Map.of("path", "../../../etc/passwd");
    // Act
    ToolResult result = tool.execute(params).join();
    // Assert
    assertTrue(result.getError().contains("traversal"));
}

Architecture Rules

Port/Adapter Boundaries

domain/ -> port/         OK
adapter/ -> port/        OK
adapter/ -> domain/      OK (models and services only)
domain/ -> adapter/      PROHIBITED

Tools

All tools implement ToolComponent. Use ToolResult.success(output) / ToolResult.failure(error).

Tools using AgentContextHolder (ThreadLocal) must NOT use CompletableFuture.supplyAsync().

Models

Domain models use @Builder. AgentContext has no no-arg constructor — always use the builder:

AgentContext context = AgentContext.builder()
        .session(session).messages(messages)
        .channel(channelPort).chatId("123")
        .build();

Build & Verify

./mvnw clean package -DskipTests   # build
./mvnw test                         # run tests
./mvnw clean verify -P strict       # full check (tests + PMD + SpotBugs)

See Also

Clone this wiki locally