Coding Standards
Critical Fullstack Rules
- Type Sharing: Always define types in shared-kernel and import from there
- API Calls: Never make direct HTTP calls - use the service layer
- Environment Variables: Access only through config objects, never process.env directly
- Error Handling: All API routes must use the standard error handler
- State Updates: Never mutate state directly - use proper state management patterns
- Company Management: Always check for existing companies before creating new ones
- File Uploads: Use presigned URLs for direct S3 uploads, never proxy through backend
- Role-Based Access: Always check user roles before displaying/executing functionality
Naming Conventions
| Element | Frontend | Backend | Example |
|---|---|---|---|
| Components | PascalCase | - | UserProfile.tsx |
| Hooks | camelCase with βuseβ | - | useAuth.ts |
| API Routes | - | kebab-case | /api/user-profile |
| API Request/Response Fields | camelCase | camelCase | firstName, eventId |
| Database Tables | - | snake_case | user_profiles |
| Company Entities | - | CompanyEntity | CompanyEntity.java |
| Domain Events | - | PascalCase + Event | EventCreatedEvent.java |
| Element | Frontend | Backend | Example |
| Enums | UPPER_CASE | UPPER_CASE (Java/API) β lowercase_snake_case (DB via AttributeConverter) | RegistrationStatus.CONFIRMED β 'confirmed' |
- Database Storage:
lowercase_snake_case(e.g.,'confirmed','speaker_brainstorming') - Java Code:
UPPER_CASE(e.g.,RegistrationStatus.CONFIRMED,EventWorkflowState.SPEAKER_BRAINSTORMING) - JSON/API Request/Response:
UPPER_CASE(e.g.,"CONFIRMED","SPEAKER_BRAINSTORMING") - Frontend TypeScript:
UPPER_CASE(e.g.,RegistrationStatus.CONFIRMED) - Conversion: JPA
@Converter(AttributeConverter) handles Java β Database. Jackson uses default serialization (UPPER_CASE).
Why this pattern?
- Keeps API consistent with Java enums (no transformation needed for JSON)
- Database uses lowercase for PostgreSQL conventions and readability
- Single conversion point (AttributeConverter) instead of multiple transformers
Git Workflow
Branch Strategy
- main - Production-ready code
- develop - Integration branch for features
- feature/{description} - Feature development
- hotfix/{description} - Critical production fixes
- release/{version} - Release preparation
Commit Message Format
type(scope): description
[optional body]
[optional footer]
Types:
feat: New featurefix: Bug fixdocs: Documentation changesstyle: Code style changesrefactor: Code refactoringtest: Adding or fixing testschore: Maintenance tasks
Examples:
feat(event-management): add automated speaker invitation workflow
fix(frontend): resolve infinite loop in event list pagination
docs(api): update OpenAPI specification for partner analytics
Code Quality Standards
Pre-commit Hooks
# Install pre-commit hooks
npm run prepare
# Hooks run automatically on commit:
# - ESLint for frontend code
# - Checkstyle for backend code
# - Unit tests for changed files
# - Format checks (Prettier/Spotless)
Code Review Checklist
- TDD Followed
- Test Coverage
Test-Driven Development Standards
TDD Workflow (Red-Green-Refactor)
MANDATORY: All new features and bug fixes must follow TDD practices
-
RED Phase: Write failing tests first
- Write E2E test for the user journey (if applicable)
- Write integration tests for API endpoints
- Write unit tests for business logic
- Verify all tests fail with meaningful error messages
-
GREEN Phase: Write minimal code to pass tests
- Implement only whatβs needed to make tests pass
- Donβt add features not covered by tests
- Focus on making it work, not perfect
-
REFACTOR Phase: Improve code while keeping tests green
- Extract methods and classes
- Remove duplication
- Improve naming and readability
- Optimize performance if needed
Test Naming Conventions
Frontend Tests:
// Pattern: should_expectedBehavior_when_condition
test('should display error message when form validation fails', () => {});
test('should navigate to dashboard when login succeeds', () => {});
Backend Tests:
// Pattern: should_expectedBehavior_when_condition
@Test
void should_createEvent_when_validRequestProvided() {}
@Test
void should_throwException_when_duplicateEventExists() {}
Test Organization Structure
frontend/
src/
components/
EventCard/
EventCard.tsx # Implementation (written after tests)
EventCard.test.tsx # Unit tests (written first)
EventCard.stories.tsx # Storybook stories
backend/
src/test/
unit/ # Fast, isolated tests
domain/ # Domain logic tests
service/ # Service layer tests
integration/ # API and database tests
controller/ # REST controller tests
repository/ # Database integration tests
e2e/ # Full workflow tests
workflows/ # Complete user journeys
Coverage Requirements
Per Acceptance Criteria:
- Each acceptance criterion must have at least one test
- Complex criteria should have multiple test cases
- Edge cases and error scenarios must be covered
Minimum Coverage Targets:
- Unit Tests: 90% for business logic
- Integration Tests: 80% for APIs
- Overall: 85% line coverage
Test-First Commit Requirements
Commit Workflow:
- First commit: Failing tests with clear test names
- Second commit: Implementation to make tests pass
- Third commit: Refactoring (if needed)
Example Commit Messages:
test(event-management): add tests for event creation workflow
feat(event-management): implement event creation to pass tests
refactor(event-management): extract event validation logic
Testing Standards
Frontend Testing
// Component tests with React Testing Library
test('displays event details correctly', () => {
render(<EventCard event={mockEvent} />);
expect(screen.getByText(mockEvent.title)).toBeInTheDocument();
});
// Hook tests
test('useAuth returns user data', () => {
const { result } = renderHook(() => useAuth());
expect(result.current.user).toBeDefined();
});
Backend Testing
CRITICAL: Production Parity for Integration Tests
All integration tests MUST use PostgreSQL via Testcontainers to ensure production parity. Never use H2 or in-memory databases for integration tests, as this creates false confidence and hides database-specific issues (e.g., JSONB types, PostgreSQL functions, etc.).
// Unit tests with JUnit 5 - fast, isolated
@Test
void shouldCreateEventSuccessfully() {
// Given
CreateEventRequest request = new CreateEventRequest("BATbern 2024");
// When
Event event = eventService.createEvent(request);
// Then
assertThat(event.getTitle()).isEqualTo("BATbern 2024");
}
// Integration tests - MUST extend AbstractIntegrationTest
@Transactional
class EventControllerIntegrationTest extends AbstractIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Test
void should_createEvent_when_validDataProvided() throws Exception {
mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{
"title": "BATbern 2024",
"eventNumber": 123,
"date": "2024-12-15T18:00:00Z",
"registrationDeadline": "2024-12-10T23:59:59Z",
"venueName": "Kornhausforum",
"venueAddress": "Kornhausplatz 18, 3011 Bern",
"venueCapacity": 200,
"organizerUsername": "john.doe"
}
"""))
.andExpect(status().isCreated());
}
}
AbstractIntegrationTest Base Class:
All integration tests should extend this base class:
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
@Testcontainers
public abstract class AbstractIntegrationTest {
// Singleton PostgreSQL container - reused across all tests for performance
@Container
static final PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test")
.withReuse(true); // Performance optimization
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
}
Test Configuration (application-test.properties):
# PostgreSQL via Testcontainers (configured dynamically)
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
# Let Flyway manage schema - validate only
spring.jpa.hibernate.ddl-auto=validate
# Enable Flyway migrations for production parity
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration
spring.flyway.baseline-on-migrate=true
Lambda Handler Testing (Infrastructure)
Lambda handlers are deployed artifacts, not just code. CDK unit tests (Template.fromStack) verify that a Lambda is declared in CloudFormation β they do not test that the handler module can load and run. These are two completely different things, and only one of them catches production failures.
Mandatory for every Lambda function:
-
Handler unit test β directly imports and calls the handler with representative inputs:
typescript// infrastructure/test/unit/lambda/<handler-name>.test.ts test('module loads without crashing', async () => { const { handler } = await import('../../../lib/lambda/my-handler/index'); expect(typeof handler).toBe('function'); }); test('passes through / returns expected shape', async () => { const { handler } = await import('../../../lib/lambda/my-handler/index'); const result = await handler(mockEvent as any, {} as any); expect(result).toMatchObject({ statusCode: 200 }); }); -
This test runs without the production dependencies installed. If it crashes on import, the Lambda will return 503 in production β that is the signal the test is designed to catch.
Bundling rule β native dependencies:
Any Lambda that uses a native binary package (sharp, pg-native, canvas, etc.) MUST use Docker for production bundling. The local tryBundle path MUST NOT include native module installation. Pattern to follow:
local: {
tryBundle(outputDir) {
if (process.env.JEST_WORKER_ID || process.env.NODE_ENV === 'test') {
// Lightweight stub for Jest β tests check CFn properties, not Lambda code
require('fs').writeFileSync(path.join(outputDir, 'index.js'), 'exports.handler = async () => ({});');
return true;
}
return false; // always use Docker for real deploys β correct Linux x64 binary
},
},
Why TypeScript and ESLint cannot catch this: Static analysis operates on source code type correctness. It has no model of what files will exist in the deployed Lambda zip. A static import nativePkg from 'native-pkg' type-checks fine; the crash happens at Lambda cold-start when the binary is missing. The handler unit test is the only pre-deploy check that catches this class of failure.
End-to-End Testing
// Playwright E2E tests
test('organizer can create and publish event', async ({ page }) => {
await page.goto('/organizer/events');
await page.click('[data-testid="create-event-button"]');
await page.fill('[data-testid="event-title"]', 'Test Event');
await page.click('[data-testid="save-event"]');
await expect(page.locator('[data-testid="event-created-message"]')).toBeVisible();
});
Documentation Standards
API Documentation
- All endpoints must have OpenAPI 3.1 specifications
- Include request/response examples
- Document all error codes and scenarios
- Provide usage examples for complex workflows
Code Documentation
/**
* Creates a new event with automated workflow initialization.
*
* @param request The event creation request containing title, date, and venue
* @return The created event with generated ID and initial status
* @throws EventValidationException if the request fails business rule validation
* @throws DuplicateEventException if an event already exists for the same quarter
*/
public Event createEvent(CreateEventRequest request) {
// Implementation
}
Architecture Documentation
- Update relevant architecture documents when making structural changes
- Include sequence diagrams for complex workflows
- Document integration patterns and dependencies
- Maintain decision records (ADRs) for significant architectural choices
JPA Performance Best Practices
Fetch Strategy Guidelines
CRITICAL: Always use LAZY fetch for collections to avoid N+1 query problems
// β
Correct - LAZY fetch (JPA default for @OneToMany/@ManyToMany)
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> children = new ArrayList<>();
// β
Correct - Explicit LAZY fetch for @ElementCollection
@ElementCollection(fetch = FetchType.LAZY)
@CollectionTable(name = "user_roles")
private Set<Role> roles = new HashSet<>();
// β
Correct - LAZY fetch for @ManyToOne
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Parent parent;
// β Wrong - EAGER causes N+1 queries
@ElementCollection(fetch = FetchType.EAGER) // DON'T DO THIS
private Set<Role> roles = new HashSet<>();
Use JOIN FETCH for Bulk Loading
When you need to load collections, use repository methods with JOIN FETCH:
// β
Correct - Repository method with JOIN FETCH
@Query("""
SELECT DISTINCT u FROM User u
LEFT JOIN FETCH u.roles
ORDER BY u.lastName ASC
""")
Page<User> findAllWithRoles(Pageable pageable);
// β Wrong - Loading entities in a loop (N+1 query)
List<User> users = userRepository.findAll();
for (User user : users) {
user.getRoles().size(); // Triggers separate query per user!
}
Database-Level Pagination
Always paginate at the database level, never in memory:
// β
Correct - Database pagination
Page<User> users = userRepository.findAll(PageRequest.of(page, limit));
// β Wrong - In-memory pagination (loads everything)
List<User> allUsers = userRepository.findAll();
List<User> pageUsers = allUsers.subList(start, end);
N+1 Query Detection
- Local Development: Enable SQL logging to spot N+1 queries
- Integration Tests: Use
@Sqlwith actual PostgreSQL (Testcontainers) - Code Review: Always check fetch strategies on
@ElementCollection,@OneToMany,@ManyToMany
# application-development.yml - Enable SQL logging
spring.jpa.show-sql: true
spring.jpa.properties.hibernate.format_sql: true
logging.level.org.hibernate.SQL: DEBUG
logging.level.org.hibernate.type.descriptor.sql.BasicBinder: TRACE
Common N+1 Pitfalls
- @ElementCollection with EAGER fetch - Most common (found in User.roles)
- Loading parent entities without JOIN FETCH - Common in list endpoints
- Accessing lazy collections in loops - Classic N+1 pattern
- DTO mapping with nested entity access - Triggers lazy loading
Performance Standards
Frontend Performance Targets
- First Contentful Paint (FCP): < 1.5s
- Largest Contentful Paint (LCP): < 2.5s
- Cumulative Layout Shift (CLS): < 0.1
- First Input Delay (FID): < 100ms
- JavaScript Bundle Size: < 250KB
Backend Performance Targets
- API Response Time (P95): < 200ms
- Database Query Time: < 50ms
- Error Rate: < 0.1%
- Uptime: > 99.9%
Performance Monitoring
// Frontend performance tracking
import { getCLS, getFID, getFCP, getLCP } from 'web-vitals';
getCLS(metric => analytics.track('CLS', metric));
getFID(metric => analytics.track('FID', metric));
getFCP(metric => analytics.track('FCP', metric));
getLCP(metric => analytics.track('LCP', metric));
// Backend performance metrics
@Timed(value = "event.creation.time", description = "Event creation time")
@Counter(value = "event.creation.count", description = "Event creation count")
public Event createEvent(CreateEventRequest request) {
// Implementation with automatic timing
}