Skip to main content

IDE Setup Best Practices

Overview

Proper IDE configuration is the foundation of developer productivity. Modern IDEs provide intelligent code completion, automated refactoring, integrated debugging, and real-time error detection that transform development workflows. This guide covers IDE setup for IntelliJ IDEA (JVM languages) and VS Code (polyglot development), including essential plugins, code style configurations, debugging setups, and local development environment orchestration.

Shared IDE configuration eliminates whitespace conflicts in code reviews and ensures consistent code formatting across teams. Version-controlled run configurations enable all developers to execute applications and tests identically. Features like live templates and keyboard-driven workflows reduce repetitive tasks from hours to seconds.


Core Principles

  1. Shared Configuration: Version control IDE settings (.editorconfig, code style files, run configurations)
  2. Consistent Formatting: Automated formatting prevents whitespace conflicts and style debates
  3. Essential Plugins Only: Plugin bloat degrades performance; install only what provides value
  4. Keyboard-Driven Workflows: Mastering shortcuts dramatically increases productivity
  5. Live Templates: Automate repetitive code patterns with minimal keystrokes
  6. Static Analysis Integration: Enable real-time linting and inspections
  7. Environment Parity: Match local development environment to production (same DB versions, runtime versions)

IntelliJ IDEA

IntelliJ IDEA is JetBrains' IDE for JVM languages, providing deep Spring Boot integration, advanced refactoring, and framework-aware code intelligence. See Spring Boot documentation for framework-specific features.

IntelliJ IDEA Ultimate (required, not Community) provides:

  • Spring Boot Integration: Dependency injection navigation, auto-configuration insights, bean lifecycle visualization
  • Database Tools: Visual query builder, schema navigation, migration execution, integrated SQL console
  • HTTP Client: Test REST APIs without external tools (alternative to Postman)
  • Profiler Integration: CPU/memory profiling with code-level granularity
  • Advanced Refactoring: Safe rename across Spring beans, extract microservice, convert to functional

Ultimate Edition's Spring Boot support includes understanding Spring-specific annotations (@Autowired, @Bean, @Configuration) to provide context-aware code completion and navigate from injection points to bean definitions instantly - essential for large Spring Boot applications with complex dependency graphs.

Essential Plugins

Install via IDE: Settings → Plugins → Marketplace → Search → Install

Required Plugins:
├── Lombok # Annotation processing for boilerplate reduction
├── SonarLint # Real-time static code analysis
├── CheckStyle-IDEA # Code style enforcement (configurable rules)
├── GitLab Integration # MR operations, CI/CD status
├── Docker # Dockerfile editing, container management
└── OpenAPI Specifications # OpenAPI/Swagger YAML editor with validation

Recommended Plugins:
├── Key Promoter X # Learn keyboard shortcuts (shows popup when using mouse)
├── Rainbow Brackets # Color-matched bracket pairs
├── String Manipulation # Case conversion, encoding/decoding
├── .ignore # .gitignore template generation
└── Material Theme UI # Modern UI theme

Lombok processes annotations like @Data, @Builder, @Slf4j to generate getters, setters, builders, and loggers at compile time - the plugin enables IDE recognition of these generated methods. SonarLint applies 600+ code quality rules in real-time, highlighting bugs, code smells, and security vulnerabilities as you type. CheckStyle enforces team-defined coding standards (naming conventions, import ordering, brace placement). GitLab Integration displays MR status, allows creating MRs, and shows CI/CD pipeline results without leaving the IDE.

Code Style Configuration

Shared code style ensures identical formatting across all developers. Commit .idea/codeStyles/Project.xml to version control so settings apply automatically when teammates clone the repository.

Import Configuration

<!-- .idea/codeStyles/Project.xml -->
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JavaCodeStyleSettings>
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="java" withSubpackages="true" static="false" />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="org.springframework" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="true" />
</value>
</option>
</JavaCodeStyleSettings>
</code_scheme>
</component>

Setting CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND to 999 prevents wildcard imports (import java.util.*). Wildcard imports hide which specific classes are used and break when new classes with conflicting names are added to imported packages. Explicit imports (import java.util.List) make dependencies clear and prevent name collision issues.

The import layout groups organize imports into logical sections separated by blank lines: Java standard library, Spring framework, application code, then static imports. This grouping improves readability and reduces merge conflicts when multiple developers add imports to the same file.

EditorConfig

EditorConfig defines formatting rules recognized by all major IDEs automatically - no IDE-specific configuration needed. Place at repository root:

# .editorconfig
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.java]
indent_style = space
indent_size = 4
max_line_length = 120

[*.{ts,tsx,js,jsx}]
indent_style = space
indent_size = 2
max_line_length = 100

[*.{yml,yaml}]
indent_style = space
indent_size = 2

[*.md]
trim_trailing_whitespace = false

end_of_line = lf enforces Unix-style line endings even on Windows, preventing line ending conflicts in Git. trim_trailing_whitespace eliminates invisible whitespace that creates diff noise. Language-specific indent sizes match community conventions: 4 spaces for Java, 2 for TypeScript/YAML. Markdown files disable trailing whitespace trimming because trailing spaces create line breaks in Markdown syntax.

Live Templates

Live templates expand abbreviations (type abbreviation, press Tab) to full code blocks with cursor placeholders. Templates use $VARIABLE$ syntax - after expansion, Tab cycles through placeholders for customization. This automates repetitive patterns while enforcing team standards.

Create Template Group

Settings → Editor → Live Templates → Add Template Group "Banking"

Payment Processing Template

Template: payment
Abbreviation: payment
Description: Payment processing method with validation, fraud check, audit
Template text:
public PaymentResult process${NAME}Payment($PARAMS$) {
log.info("Processing ${NAME} payment for user {}", userId);

// Validate payment
validatePayment(payment);

// Check fraud
fraudService.checkFraud(payment);

// Execute payment
PaymentResult result = executePayment(payment);

// Audit log
auditLogger.logPaymentProcessed(payment, result);

return result;
}

Type payment, press Tab, cursor jumps to ${NAME} - enter payment type ("Transfer", "Bill"). Tab again moves to $PARAMS$ for parameter definition. This template encodes the standard pattern: validation → fraud checking → execution → audit logging. All payment processing methods follow this structure, ensuring critical steps (fraud check, audit) are never forgotten.

Test Method Template

Template: test
Abbreviation: test
Description: JUnit 5 test method with Given-When-Then structure
Template text:
@Test
void should${NAME}_when${CONDITION}() {
// Given
$GIVEN$

// When
$WHEN$

// Then
$THEN$
}

This template enforces Given-When-Then structure. Method names follow shouldX_whenY convention, making test intent explicit: shouldRejectPayment_whenInsufficientFunds(). The three-section structure separates test setup (Given), action under test (When), and verification (Then), improving test readability and maintainability. See Java Testing Guidelines for detailed testing patterns.

Keyboard Shortcuts

Master these shortcuts to eliminate mouse usage and accelerate workflows:

ActionWindows/LinuxPurpose
Navigation
Go to classCtrl + NJump to any class by name (supports fuzzy search)
Go to fileCtrl + Shift + NJump to any file (resources, configs, etc.)
Go to symbolCtrl + Alt + Shift + NJump to method/field across project
Recent filesCtrl + ESwitch between recently opened files
Navigate back/forwardCtrl + Alt + ←/→Navigate cursor history (like browser back/forward)
Editing
Duplicate lineCtrl + DDuplicate current line or selection
Delete lineCtrl + YDelete current line (no copy to clipboard)
Move line up/downAlt + Shift + ↑/↓Move line while maintaining indentation
Format codeCtrl + Alt + LApply code style formatting
Optimize importsCtrl + Alt + ORemove unused imports, organize order
Refactoring
RenameShift + F6Safe rename (updates all references)
Extract methodCtrl + Alt + MExtract selected code to new method
Extract variableCtrl + Alt + VExtract expression to variable
InlineCtrl + Alt + NInline variable/method (opposite of extract)
Code Generation
GenerateAlt + InsertGenerate getters/setters/constructors/toString
Override methodsCtrl + OOverride superclass methods
Implement methodsCtrl + IImplement interface methods
Search
Find in pathCtrl + Shift + FProject-wide text search
Replace in pathCtrl + Shift + RProject-wide find and replace
Run/Debug
RunShift + F10Run current configuration
DebugShift + F9Debug current configuration
Toggle breakpointCtrl + F8Add/remove breakpoint at current line
Evaluate expressionAlt + F8Evaluate expression during debugging

The "Go to class/file/symbol" shortcuts use fuzzy matching: type "PaSe" to find "PaymentService". This eliminates scrolling through package hierarchies. Extract method refactoring (Ctrl + Alt + M) automatically detects required parameters and return types, making code organization effortless.

Run Configurations

Shared run configurations ensure all developers execute applications identically. Commit to .idea/runConfigurations/ for version control.

Spring Boot Application

<!-- .idea/runConfigurations/PaymentService.xml -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="PaymentService" type="SpringBootApplicationConfigurationType">
<option name="SPRING_BOOT_MAIN_CLASS" value="com.bank.payment.PaymentApplication" />
<module name="payment-service" />
<option name="ACTIVE_PROFILES" value="dev" />
<option name="VM_PARAMETERS" value="-Xmx512m -Dspring.profiles.active=dev" />
<option name="PROGRAM_PARAMETERS" value="" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

Activating the dev profile loads application-dev.yml for development-specific configuration (local database URLs, debug logging). The -Xmx512m heap limit matches production container constraints, surfacing memory issues during development rather than production. The Make option compiles code before running, ensuring latest changes execute.

JUnit Tests

<!-- .idea/runConfigurations/AllTests.xml -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="All Tests" type="JUnit">
<module name="payment-service" />
<option name="PACKAGE_NAME" value="" />
<option name="MAIN_CLASS_NAME" value="" />
<option name="METHOD_NAME" value="" />
<option name="TEST_OBJECT" value="package" />
<option name="VM_PARAMETERS" value="-ea" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>

The -ea flag enables assertions. Java disables assertions by default for performance, but tests should validate assert statements: assert result.isValid() : "Result validation failed". Assertions provide runtime invariant checking that complements unit tests.

Remote Debugging

Remote debugging connects your IDE to applications running elsewhere (Docker, Kubernetes, remote servers), enabling full debugging capabilities (breakpoints, variable inspection, step-through) against live environments.

Enable Debug Port

Start Java applications with JDWP (Java Debug Wire Protocol) enabled:

# Start with debug port enabled
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 \
-jar payment-service.jar

# Docker: expose debug port
docker run -p 8080:8080 -p 5005:5005 payment-service

# Kubernetes: port forward to local machine
kubectl port-forward pod/payment-service-abc123 5005:5005

The -agentlib:jdwp parameters: transport=dt_socket uses TCP sockets, server=y makes the JVM listen for debugger connections, suspend=n starts the application normally without waiting (use suspend=y to pause at startup for debugging initialization code), address=*:5005 listens on all interfaces on port 5005.

Kubernetes port-forwarding creates a tunnel from localhost:5005 to the pod's port 5005, making the remote debug port appear local. See Docker and Kubernetes documentation for container debugging patterns.

Debug Configuration

<!-- .idea/runConfigurations/RemoteDebug.xml -->
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Remote Debug" type="Remote">
<option name="USE_SOCKET_TRANSPORT" value="true" />
<option name="SERVER_MODE" value="false" />
<option name="SHMEM_ADDRESS" />
<option name="HOST" value="localhost" />
<option name="PORT" value="5005" />
<option name="AUTO_RESTART" value="false" />
<RunnerSettings RunnerId="Debug">
<option name="DEBUG_PORT" value="5005" />
<option name="LOCAL" value="false" />
</RunnerSettings>
</configuration>
</component>

After port-forwarding and starting this configuration (Shift + F9), set breakpoints in your IDE - execution pauses when the remote application hits those lines. This enables debugging environment-specific issues that don't reproduce locally.


VS Code

Visual Studio Code is a lightweight, extensible editor excelling at polyglot development (Java, TypeScript, Python, Go) with faster startup and lower memory footprint than IntelliJ. It's particularly strong for frontend development (React, Angular) with excellent Git integration.

Essential Extensions

Create .vscode/extensions.json and commit to version control. VS Code automatically recommends these extensions when teammates open the project:

{
"recommendations": [
// Java
"vscjava.vscode-java-pack",
"vscjava.vscode-spring-boot-dashboard",
"redhat.java",
"vscjava.vscode-maven",

// TypeScript/JavaScript
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss",

// Git
"eamodio.gitlens",
"gitlab.gitlab-workflow",

// Docker
"ms-azuretools.vscode-docker",

// Database
"cweijan.vscode-database-client2",

// API
"42crunch.vscode-openapi",
"humao.rest-client",

// Code Quality
"sonarsource.sonarlint-vscode",
"streetsidesoftware.code-spell-checker",

// Productivity
"gruntfuggly.todo-tree",
"usernamehw.errorlens",
"wayou.vscode-todo-highlight"
]
}

GitLens provides inline Git blame annotations, commit history visualization, and repository comparisons. ErrorLens displays linting errors inline (next to code) rather than requiring hovering, enabling faster error identification. TODO Tree highlights TODO/FIXME comments across the project for quick navigation.

Settings Configuration

Commit .vscode/settings.json to enforce team-wide editor behavior:

{
// .vscode/settings.json

// Editor
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true,
"source.fixAll.eslint": true
},
"editor.rulers": [100, 120],
"editor.tabSize": 2,
"editor.insertSpaces": true,

// Java
"java.configuration.updateBuildConfiguration": "automatic",
"java.saveActions.organizeImports": true,
"java.format.settings.url": ".vscode/eclipse-formatter.xml",

// TypeScript
"typescript.updateImportsOnFileMove.enabled": "always",
"typescript.preferences.importModuleSpecifier": "relative",

// Prettier
"prettier.singleQuote": true,
"prettier.trailingComma": "es5",
"prettier.printWidth": 100,

// ESLint
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],

// Files
"files.exclude": {
"**/.git": true,
"**/.DS_Store": true,
"**/node_modules": true,
"**/target": true
},
"files.watcherExclude": {
"**/target/**": true,
"**/node_modules/**": true
},

// Search
"search.exclude": {
"**/node_modules": true,
"**/target": true,
"**/build": true,
"**/.git": true
}
}

formatOnSave and organizeImports eliminate manual formatting - saving a file automatically applies Prettier/ESLint rules and removes unused imports. editor.rulers displays vertical lines at 100 and 120 characters for visual line length guidance. Excluding node_modules and target from search and file watching improves performance dramatically in large projects.

Launch Configurations

Define application launch and debugging in .vscode/launch.json:

{
// .vscode/launch.json
"version": "0.2.0",
"configurations": [
{
"type": "java",
"name": "PaymentApplication",
"request": "launch",
"mainClass": "com.bank.payment.PaymentApplication",
"projectName": "payment-service",
"args": "",
"vmArgs": "-Dspring.profiles.active=dev",
"env": {
"SPRING_PROFILES_ACTIVE": "dev"
}
},
{
"type": "java",
"name": "Debug (Attach)",
"request": "attach",
"hostName": "localhost",
"port": 5005
},
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/src/index.ts",
"preLaunchTask": "tsc: build - tsconfig.json",
"outFiles": ["${workspaceFolder}/dist/**/*.js"]
}
]
}

The "Debug (Attach)" configuration connects to remote debug ports (same as IntelliJ remote debugging). The Node.js configuration runs a preLaunchTask to compile TypeScript before launching, ensuring the latest code executes.

Tasks

Tasks define common build/test operations in .vscode/tasks.json:

{
// .vscode/tasks.json
"version": "2.0.0",
"tasks": [
{
"label": "Gradle: build",
"type": "shell",
"command": "./gradlew build -x test --no-daemon",
"group": "build",
"problemMatcher": []
},
{
"label": "Gradle: test",
"type": "shell",
"command": "./gradlew test --no-daemon",
"group": "test",
"problemMatcher": []
},
{
"label": "npm: install",
"type": "npm",
"script": "install",
"group": "build"
},
{
"label": "npm: test",
"type": "npm",
"script": "test",
"group": "test"
}
]
}

Tasks execute via Command Palette (Ctrl + Shift + P → "Tasks: Run Task"). The problemMatcher parses command output to highlight errors in the editor. Tasks can serve as preLaunchTask in launch configurations for compilation before running.

Keyboard Shortcuts

ActionShortcutPurpose
Navigation
Go to fileCtrl + PQuick file navigation with fuzzy search
Go to symbolCtrl + Shift + ONavigate to symbols in current file
Go to definitionF12Jump to symbol definition
Peek definitionAlt + F12View definition inline (no navigation)
Find referencesShift + F12Find all references to symbol
Editing
Duplicate lineAlt + Shift + ↓Duplicate current line downward
Delete lineCtrl + Shift + KDelete current line
Move line up/downAlt + ↑/↓Move line while maintaining indentation
Format documentShift + Alt + FApply formatter (Prettier/ESLint)
Multi-Cursor
Add cursor above/belowCtrl + Alt + ↑/↓Add cursors to adjacent lines
Select all occurrencesCtrl + Shift + LAdd cursor to all occurrences of selection
Refactoring
Rename symbolF2Rename symbol across project
Extract methodCtrl + Shift + RExtract selection to new function
Command Palette
Show commandsCtrl + Shift + PAccess all VS Code commands
Terminal
Toggle terminalCtrl + `Show/hide integrated terminal
New terminalCtrl + Shift + `` ``Create new terminal instance

Multi-cursor editing (Ctrl + Shift + L) enables editing multiple identical occurrences simultaneously - invaluable for renaming variables across a function or updating multiple similar lines.


Local Development Environment

Environment parity minimizes differences between development and production, reducing "works on my machine" bugs. The Twelve-Factor App methodology defines parity across three dimensions: time (deploy quickly), personnel (developers deploy), and tools (same technologies everywhere).

Docker Compose for Local Dependencies

Docker Compose orchestrates multi-container applications, starting all service dependencies (databases, caches, message brokers) with consistent configuration. Running docker-compose up -d starts all dependencies in the background; docker-compose down -v destroys everything for a clean slate. See Docker best practices for optimization.

Compose Configuration

# docker-compose.yml
version: '3.8'

services:
postgres:
image: postgres:16-alpine
container_name: dev-postgres
environment:
POSTGRES_DB: payments
POSTGRES_USER: paymentuser
POSTGRES_PASSWORD: devpassword
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U paymentuser"]
interval: 10s
timeout: 5s
retries: 5

redis:
image: redis:7-alpine
container_name: dev-redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes

kafka:
image: confluentinc/cp-kafka:latest
container_name: dev-kafka
depends_on:
- zookeeper
ports:
- "9092:9092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1

zookeeper:
image: confluentinc/cp-zookeeper:latest
container_name: dev-zookeeper
ports:
- "2181:2181"
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000

volumes:
postgres_data:
redis_data:

Port mappings (5432:5432) expose container ports to the host, enabling applications to connect via localhost:5432. Volume mounts persist data between restarts - deleting containers doesn't lose database data. Health checks (pg_isready) ensure PostgreSQL fully initializes before dependent services start. See Spring Boot configuration for connecting to these services.

Database Initialization

The init-db.sql script runs automatically when PostgreSQL first starts via Docker's /docker-entrypoint-initdb.d/ directory, creating schemas and seed data:

-- scripts/init-db.sql
CREATE TABLE IF NOT EXISTS payments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL,
amount DECIMAL(19, 4) NOT NULL,
currency VARCHAR(3) NOT NULL,
status VARCHAR(20) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_payments_user_id ON payments(user_id);
CREATE INDEX idx_payments_status ON payments(status);

-- Seed test data
INSERT INTO payments (user_id, amount, currency, status)
VALUES
('123e4567-e89b-12d3-a456-426614174000', 100.00, 'USD', 'COMPLETED'),
('123e4567-e89b-12d3-a456-426614174001', 250.50, 'EUR', 'PENDING');

Initialization scripts provide consistent starting state for all developers. Use Flyway migrations for production schema evolution, but initialization scripts bootstrap local development quickly.

Usage Commands

# Start all services (first time: pulls images, creates volumes)
docker-compose up -d

# View logs from all services (follow mode)
docker-compose logs -f

# View logs from specific service
docker-compose logs -f postgres

# Stop services (preserves volumes/data)
docker-compose stop

# Stop and remove containers (preserves volumes)
docker-compose down

# Stop, remove containers, and delete volumes (fresh start)
docker-compose down -v

# Rebuild containers after changing Dockerfiles
docker-compose up -d --build

For projects with environment-specific overrides: docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d.

Hot Reload Configuration

Hot reload automatically restarts/refreshes applications when source files change, reducing feedback time from minutes to seconds.

Spring Boot DevTools

Spring Boot DevTools uses two classloaders: one for dependencies (unchanged, not reloaded) and one for application code (reloaded). Only the application classloader restarts, making reloads 10x faster than full restarts.

// build.gradle
developmentOnly 'org.springframework.boot:spring-boot-devtools'
# application-dev.yml
spring:
devtools:
restart:
enabled: true
additional-paths: src/main/resources
livereload:
enabled: true

IntelliJ configuration: Enable "Build project automatically" (Settings → Build, Execution, Deployment → Compiler) and "Allow auto-make to start even if developed application is currently running" (Settings → Advanced Settings). This recompiles classes while the application runs, triggering DevTools restart.

React Fast Refresh

React Fast Refresh preserves component state while reloading changed components, maintaining form inputs, scroll position, and UI state. Enabled by default in Create React App and Vite. See React development for component patterns.

// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
plugins: [react()],
server: {
port: 3000,
host: true, // Expose to network for mobile device testing
hmr: {
overlay: true // Show error overlay in browser
}
}
});

Fast Refresh falls back to full page reload on syntax errors or changes to top-level exports - the browser console explains why.

Angular Hot Module Replacement

Angular CLI enables HMR with --hmr flag, reloading changed modules without full page refresh. Unlike React, Angular HMR doesn't preserve component state by default. See Angular development for change detection optimization.

# Start with HMR
ng serve --hmr

# Or configure in angular.json for automatic HMR

API Proxying

During development, frontend applications often call backend APIs running elsewhere. Proxy configuration redirects API requests from the frontend dev server to the backend, avoiding CORS issues:

Vite Proxy (React)

// vite.config.ts
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
});

Frontend code uses relative URLs (fetch('/api/payments')). The dev server intercepts /api requests and proxies to http://localhost:8080, rewriting /api/payments to /payments. changeOrigin modifies the Host header to match the target, required for some backends.

Angular Proxy

// proxy.conf.json
{
"/api": {
"target": "http://localhost:8080",
"secure": false,
"changeOrigin": true,
"logLevel": "debug"
}
}
# Start Angular with proxy
ng serve --proxy-config proxy.conf.json

Mock Services

Mock services simulate external dependencies when real services are unavailable, unstable, or expensive. Mocks provide predictable responses, support specific test scenarios, and eliminate network latency.

WireMock for HTTP APIs

WireMock creates HTTP mocks with request matchers and response templates. It supports stateful scenarios, fault injection (delays, errors), and request verification:

// WireMock in tests
@Test
void testPaymentServiceWithMock() {
WireMockServer wireMock = new WireMockServer(8089);
wireMock.start();

// Configure mock response
wireMock.stubFor(get(urlEqualTo("/api/exchange-rate/USD/EUR"))
.willReturn(aResponse()
.withHeader("Content-Type", "application/json")
.withBody("{\"rate\": 0.85}")
.withStatus(200)));

// Application calls http://localhost:8089/api/exchange-rate/USD/EUR
// and receives mocked response
}

WireMock is valuable when integrating third-party APIs (payment gateways, KYC providers) that charge per request or have rate limits. Develop against mocks locally, switch to real APIs in staging/production. Standalone WireMock runs via Docker:

# Run WireMock standalone
docker run -p 8080:8080 -v $PWD/mappings:/home/wiremock/mappings wiremock/wiremock
// mappings/exchange-rate.json
{
"request": {
"method": "GET",
"urlPattern": "/api/exchange-rate/([A-Z]{3})/([A-Z]{3})"
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"jsonBody": {
"rate": 0.85,
"timestamp": "{{now}}"
}
}
}

JSON Server for Quick Prototypes

JSON Server creates a full REST API from a JSON file, ideal for frontend development when the backend API isn't ready:

// db.json
{
"payments": [
{
"id": 1,
"userId": "user-123",
"amount": 100.00,
"currency": "USD",
"status": "COMPLETED"
}
]
}
# Install and run
npm install -g json-server
json-server --watch db.json --port 3001

# Available endpoints automatically generated:
# GET /payments
# GET /payments/1
# POST /payments
# PUT /payments/1
# DELETE /payments/1

# Filtering: GET /payments?status=COMPLETED
# Sorting: GET /payments?_sort=amount&_order=desc

JSON Server supports relationships, full-text search, and custom routes. Not suitable for production but excellent for rapid frontend prototyping.

Environment Parity Best Practices

Match Production Versions

Use identical database, cache, and runtime versions in development and production. Version mismatches cause subtle bugs - PostgreSQL 14 to 15 changed authentication methods and added SQL features. Code developed against 14 might fail on 15.

# docker-compose.yml - Pin exact versions (not :latest)
services:
postgres:
image: postgres:16.4-alpine # Exact version

redis:
image: redis:7.0.11-alpine # Exact version

java:
image: eclipse-temurin:21-jre-jammy # Matches production JRE

Avoid :latest tags - they make builds non-reproducible. :latest today differs from :latest next month. Pin versions and update deliberately after testing.

Configuration via Environment Variables

Use environment variables for configuration, never hardcode values. This allows identical application code to run everywhere with environment-specific configuration:

# application.yml (common across all environments)
spring:
datasource:
url: ${DATABASE_URL}
username: ${DATABASE_USERNAME}
password: ${DATABASE_PASSWORD}
kafka:
bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS}
# .env file for local development (not committed to Git)
DATABASE_URL=jdbc:postgresql://localhost:5432/payments
DATABASE_USERNAME=paymentuser
DATABASE_PASSWORD=devpassword
KAFKA_BOOTSTRAP_SERVERS=localhost:9092

Production environments inject configuration via Kubernetes ConfigMaps/Secrets, but application code remains identical. See Spring Boot configuration management and Secrets Management.


Anti-Patterns

IDE Configuration Anti-Patterns

Not Version Controlling Settings: Committing only code without IDE configurations forces each developer to manually configure their IDE, leading to inconsistent formatting and frequent whitespace conflicts.

Over-Reliance on Mouse: Using the mouse for actions with keyboard shortcuts slows development dramatically. Force yourself to use shortcuts - install Key Promoter X to learn them.

Plugin Bloat: Installing too many plugins degrades IDE performance. Only install plugins providing clear value; uninstall unused ones.

Ignoring Code Formatting Warnings: Disabling or ignoring formatting warnings leads to inconsistent code style. Fix formatting issues immediately; better yet, enable format-on-save.

Hardcoding Configuration: Hardcoding database URLs, API endpoints, or credentials in code requires changing code to run in different environments. Use environment variables and externalized configuration.

Using :latest Docker Tags: Using :latest tags makes builds non-reproducible and can introduce breaking changes unexpectedly. Always pin specific versions.

Skipping Hot Reload Setup: Manually stopping/restarting applications wastes time. Configure hot reload to reduce feedback loops from minutes to seconds.

Not Matching Production Versions: Running different database or runtime versions locally than production leads to "works on my machine" bugs. Match production versions exactly.

Manual Data Seeding: Manually creating test data through UI for each developer is time-consuming and error-prone. Automate with SQL scripts or factory libraries.

Ignoring Resource Limits: Running Docker containers without resource limits can consume all system resources, degrading performance. Set memory/CPU limits matching production constraints.


Examples

Complete IntelliJ Setup Workflow

# 1. Clone repository
git clone https://gitlab.com/bank/payment-service.git
cd payment-service

# 2. Start local dependencies
docker-compose up -d

# 3. Open project in IntelliJ IDEA
# File → Open → Select payment-service directory

# 4. Wait for Gradle sync and indexing

# 5. Install recommended plugins (Settings → Plugins)
# - Lombok, SonarLint, CheckStyle-IDEA, GitLab Integration

# 6. Code style automatically applied from .idea/codeStyles/Project.xml

# 7. Run configuration automatically available from .idea/runConfigurations/

# 8. Run application (Shift + F10)
# Application starts with dev profile, connects to Docker Compose databases

# 9. Run tests (Ctrl + Shift + F10 on test class)

Complete VS Code Setup Workflow

# 1. Clone repository
git clone https://gitlab.com/bank/payment-service.git
cd payment-service

# 2. Start local dependencies
docker-compose up -d

# 3. Open in VS Code
code .

# 4. Install recommended extensions (notification appears)
# Click "Install All" on extension recommendations

# 5. Settings automatically applied from .vscode/settings.json

# 6. Run configuration available in Run panel (Ctrl + Shift + D)

# 7. Start debugging (F5)

Local Development with Mock Services

# 1. Start dependencies and mocks
docker-compose -f docker-compose.yml -f docker-compose.mocks.yml up -d

# 2. Configure application to use mock endpoints
export EXCHANGE_RATE_API_URL=http://localhost:8089/api/exchange-rate
export FRAUD_SERVICE_URL=http://localhost:8089/api/fraud-check

# 3. Run application
./gradlew bootRun --no-daemon

# 4. Application calls mocks instead of real external services
# Develop without API rate limits or charges

Further Reading

Internal Documentation

External Resources