Developer guidelines usually start life as team checklists, but those checklists only help when people can understand the reasoning behind them. This post turns a broad set of engineering standards into a practical field guide: how to pick ready work, write code that stays maintainable, operate services safely, and ship changes with fewer avoidable mistakes.
The goal is not to create a process for its own sake. The goal is to reduce the classes of problems that repeatedly slow teams down: unclear story scope, oversized classes, poor logging, unsafe schema changes, brittle APIs, weak CI gates, and preventable security issues.
How to Use This Guide
- Use it when starting a new story to check readiness and definition of done.
- Use it during implementation to keep code, database, and API decisions aligned with team standards.
- Use it in code review to spot issues before they become production problems.
- Use it as onboarding material for engineers who need the team’s default expectations in one place.
General
These are the habits that shape delivery quality before code is even merged.
| Guideline | Notes |
|---|---|
| Pick a story only if it has Acceptance Criteria | No AC = not ready for dev |
| Estimate including unit and integration testing | Don’t estimate just implementation time |
| Strive for TDD | See TDD guide |
| Install the right dev tools and plugins | See Developer Tools below |
| Document ADR for architectural decisions | One ADR per significant decision |
Keep README.md up to date | |
| Define “Dev Done” as a team | Example: story is dev-done only when deployed to dev env and showcased to QA/BA |
Why this matters: Teams lose time when work starts before the story is clear or when “done” means different things to different people. A small amount of discipline here prevents churn later.
Object-Oriented Design
This section focuses on maintainability. Most long-term codebase pain comes from weak boundaries, unclear names, and methods that do too much.
OOP Checklist
| Principle | Notes | Reference |
|---|---|---|
| Tell-Don’t-Ask | Objects should do things, not expose state for callers to act on | Martin Fowler |
| DRY | Don’t Repeat Yourself – duplication is a maintenance burden | |
| Single Responsibility | One reason to change per class/method | |
| Intention-revealing names | Names explain what, not how | |
| Consistent formatting | Use auto-formatters – remove subjective debates | |
| Null Object pattern | Avoids null checks scattered everywhere | |
| Avoid deeply nested code | Extract methods to flatten nesting > 3 levels | Code smell reference |
| Avoid dead code | Delete it; don’t comment it out | |
| Methods ≤ 30 lines | Long methods are a refactoring signal | |
| Classes ≤ 200 lines | reference | |
| Avoid long parameter lists | Pass value objects or builders instead | |
| Replace conditionals with polymorphism | Use Strategy or Visitor pattern | |
| Don’t swallow exceptions | Always log or rethrow with context | reference |
| Prefer composition over inheritance | Favour has-a over is-a | |
| Anti-corruption layer for external systems | Isolate external API shapes from your domain | reference |
| Every class with behaviour has a unit test | Aim for 100% branch coverage |
Code Example: Tell-Don’t-Ask
// ❌ BAD — asks for state, then acts on it outside the objectif (order.getStatus().equals("PENDING") && order.getTotal() > 0) { order.setStatus("PROCESSING"); paymentService.charge(order.getTotal());}// ✅ GOOD — tells the order to process itselforder.process(paymentService);// Order encapsulates the status check and state transition
Code Example: Extract Small Methods
// ❌ BAD — 40-line method doing 4 thingspublic void handleOrder(Order order) { // 10 lines of validation // 10 lines of pricing calculation // 10 lines of inventory check // 10 lines of payment processing}// ✅ GOOD — each concern is named and testablepublic void handleOrder(Order order) { validateOrder(order); Price price = calculatePrice(order); reserveInventory(order); processPayment(order, price);}
Key takeaway: Keep behaviour close to the object that owns it, keep units of code small, and optimise for code that is easy to read and safe to change.
Logging
Logs are not just debugging output. They are operational evidence when something goes wrong in production.
| Guideline | Notes |
|---|---|
Use INFO, ERROR, DEBUG appropriately | Entry/exit → DEBUG; all exceptions → ERROR/WARN |
| Enable distributed logging | Configure Istio to propagate trace headers; log the tracing header in MDC context |
| Never log PII | Mask names, emails, account numbers, card numbers before logging |
| Include request/trace ID in every log line | Essential for tracing a request across services |
Log Level Guide
// DEBUG — developer detail, disabled in productionlog.debug("Entering calculateDiscount with orderId={}", orderId);// INFO — normal business eventslog.info("Order processed successfully. orderId={}, amount={}", orderId, amount);// WARN — unexpected state, service still workinglog.warn("Payment retry attempt {} for orderId={}", retryCount, orderId);// ERROR — exception caught, operation failedlog.error("Failed to process payment for orderId={}. Cause: {}", orderId, e.getMessage(), e);
Danger: ANTI-PATTERN
// ❌ Never log PIIlog.info("User {} with email {} placed order", user.getName(), user.getEmail());// ✅ Log IDs onlylog.info("User {} placed order {}", user.getId(), orderId);
Key takeaway: good logs help you reconstruct what happened across services without exposing sensitive data.
Database
Database changes deserve extra caution because application versions often overlap during deployment. A schema that works only for the newest code is not safe enough.
| Guideline | Notes |
|---|---|
| Use schema evolution tools | Flyway or Liquibase – never manual schema changes |
| Naming conventions | lower_case_underscore_separated for tables and columns; be consistent with plural vs singular |
| Configure connection pool | |
| Be intentional about indexes | Create only based on actual access patterns – use EXPLAIN ANALYSE |
| Plan for data archival | Growing tables need an archival strategy before they become a problem |
Index Creation Rule
Create an index when a column appears in WHERE, JOIN ON, or ORDER BY on a table with > 10k rows. Verify with EXPLAIN ANALYSE:
EXPLAIN ANALYSE SELECT * FROM orders WHERE user_id = 123;-- Look for "Seq Scan" on large tables — that's a missing index-- "Index Scan" means the index is being used
Common Mistakes to Avoid
- Adding schema changes manually in production instead of through versioned migrations
- Creating indexes without confirming an actual query pattern or execution plan needs
- Ignoring table growth until cleanup, partitioning, or archival becomes urgent
Key takeaway: Treat schema design as an operational concern, not just a modelling exercise.
REST / Events
Interfaces outlive implementations. API and event conventions need to be predictable because other systems build against them.
| Guideline | Notes |
|---|---|
| Follow RESTful URL conventions | Resource-based, lowercase, hyphenated: /order-items, not /getOrderItems |
| Standard response/error codes | 200 OK, 201 Created, 400 Bad Request, 404 Not Found, 409 Conflict, 500 Internal Server Error |
| Integration test all controllers | Cover 2XX, 4XX, and 5XX paths |
| Use RFC 3339 date-time format | 2024-01-15T10:30:00Z – not 15/01/2024 or epoch milliseconds |
REST Naming Conventions
# ✅ GoodGET /orders → list ordersGET /orders/{id} → get one orderPOST /orders → create orderPUT /orders/{id} → replace orderPATCH /orders/{id} → partial updateDELETE /orders/{id} → delete order# ❌ BadGET /getOrdersPOST /createOrderGET /orders/getById/{id}
Key takeaway: Stable, conventional interfaces reduce client confusion and make integrations easier to test and maintain.
HTTP Client
Outbound calls are one of the fastest ways for a healthy service to become unhealthy. Defaults are often unsafe.
| Guideline | Notes |
|---|---|
| Configure all three timeouts | Connection timeout (establish TCP), read timeout (wait for response), write timeout (send request body) |
| Configure circuit breakers | Open circuit after N failures; half-open to probe recovery |
| Configure HTTP connection pool | Prevent connection exhaustion under load |
Timeout Configuration Example (Spring RestTemplate / WebClient)
// ✅ Configure all timeouts — never use defaultsHttpClient httpClient = HttpClient.create() .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) // 5s connect .responseTimeout(Duration.ofSeconds(10)) // 10s read .doOnConnected(conn -> conn.addHandlerLast(new ReadTimeoutHandler(10)) .addHandlerLast(new WriteTimeoutHandler(10)));
Danger: ANTI-PATTERN
Using default HTTP client timeouts (often infinite).
An external service that hangs will hold your thread indefinitely. Under load, this exhausts your thread pool and cascades into a full outage. Always set explicit timeouts.
Key takeaway: Every external dependency needs timeout, pooling, and failure-isolation settings before it is production-ready.
CI Pipeline
The CI pipeline is the automated version of team discipline. If quality checks are optional, they eventually become inconsistent.
| Stage | Requirement | Why |
|---|---|---|
| Lint | Checkstyle, PMD, SpotBugs – fail on violations | Consistent style without review arguments |
| Sonar | Fail on quality gate violations | Catches bugs, duplication, and coverage regression |
| Test | Fail if coverage < agreed threshold | Regression protection |
| Build | Produce the build artifact | |
| Audit | DependencyCheck / Clair / Veracode | Catch known CVEs before they reach production |
| Package | Output Docker image | Immutable, versioned artifact |
| Push | Push to registry | |
| Security Analysis | Trivy image scan | Catch OS-level vulnerabilities in base image |
Key takeaway: CI should block low-quality or risky changes early, before they become review fatigue or production incidents.
Code Repository Structure
Repository structure affects how quickly engineers can understand, navigate, and safely change a service.
| Requirement | Notes |
|---|---|
Detailed README.md | Service purpose, architecture links, local setup, Swagger link, tech stack versions |
| Segregate packages by domain | com.company.orders.pricing, not com.company.controllers |
| Linting tools configured | PMD, Checkstyle, SpotBugs, CPD (copy-paste detector), SonarQube |
| Descriptive service name | Name the repo after what it does |
Key takeaway: Organise repositories around business capability and discoverability, not convenience for the first developer who created them.
Security
Security guidelines are engineering defaults, not a separate phase at the end of delivery.
| Requirement | Link |
|---|---|
| Install Talisman as a pre-commit hook | Prevents committing secrets, API keys, passwords |
| Run dependency scanner in CI | DependencyChecker, Clair, or Veracode |
| Follow OWASP Top 10 | OWASP Top 10 |
| Sanitise all inputs | Prevent SQL injection, XSS, path traversal at system boundaries |
| Never hardcode secrets | Use environment variables or a secrets manager |
Key takeaway: Most avoidable security issues come from weak defaults, not advanced attacks. Build the safe path into normal development.
Developer Tools and Plugins
Tooling should remove friction, not add ceremony. The right defaults improve code quality before review starts.
IntelliJ IDEA recommended plugins:
| Plugin | Purpose |
|---|---|
| SonarLint | Real-time code quality feedback |
| Checkstyle | Style enforcement |
| PMD-IDEA | Static analysis |
| SpotBugs | Bug detection |
| Rainbow Brackets | Nesting clarity |
| Key Promoter X | Learn keyboard shortcuts |
| diffplug/spotless | Auto-formatting |
Terminal: iTerm2 + Zsh + Oh My Zsh
Mocking Services
When dependencies are unavailable, expensive, or hard to control, service virtualisation keeps development and testing moving.
| Use Case | Tool |
|---|---|
| Static responses | WireMock |
| Dynamic / scenario-based responses | Mountebank |
Clean Code References
Final Takeaways
- Start only with stories that are clear, testable, and aligned with what “done” means.
- Keep code small, intention-revealing, and easy to change.
- Treat logging, database changes, and external calls as operational design concerns.
- Make interfaces predictable and CI enforcement non-optional.
- Build security and maintainability into the default workflow instead of relying on heroics later.
- Refactoring Catalogue – Fowler
- Clean Code Summary Cheatsheet
Leave a Reply