Comprehensive Software Development Guidelines
Table of Contents
General Coding Guidelines
Naming and Organization
- Naming Conventions
- Use descriptive, intention-revealing names for variables, methods, and classes
- Avoid abbreviations unless universally understood
- Follow language-specific conventions (camelCase, PascalCase, etc.)
- Name boolean variables with prefixes like “is”, “has”, or “should”
- Code Organization
- Separate static and dynamic parts of your application
- Follow a consistent, logical file and directory structure
- Group related functionality together
- Keep source files focused on a single responsibility
Coding Practices
- No Hard Coding
- Define constants or enums in appropriate locations
- Use configuration files for environment-specific values
- Centralize configuration management
- Simplicity and Complexity
- Prefer simplicity over complexity
- If code becomes complex, it’s likely a candidate for refactoring
- Remember: “It’s hard to build simple things”
- Complex solutions usually indicate design issues
- Optimization
- Avoid premature optimization
- Optimize only after profiling and identifying actual bottlenecks
- Consider algorithmic efficiency (Big O notation) for critical paths
- Design Patterns
- Look for opportunities to apply standard design patterns
- Adapt patterns to fit your specific use case
- Document pattern usage for clarity
- Code Repetition
- Strictly prohibit repetitive code (DRY – Don’t Repeat Yourself)
- Extract repeated logic into reusable methods or classes
- Consider refactoring when similar code appears multiple times
- Code Formatting
- Consistently align and indent code
- Follow language/framework-specific style guides
- Use automated formatting tools
- Always format before committing code
Class Design
- Size and Responsibility
- Keep classes under 600 lines (smaller is better)
- Follow Single Responsibility Principle
- Each class should have one clear purpose
- Constructors
- Keep constructors simple and exception-safe
- Use dependency injection for dependencies
- Initialize essential state only
- Composition vs Inheritance
- Prefer composition over inheritance
- Use inheritance only for genuine “is-a” relationships
- Avoid deep inheritance hierarchies
- Extensibility
- Design classes for extensibility
- Implement the Open/Closed Principle
- Consider future use cases without overengineering
Functions and Methods
- Size and Complexity
- Keep functions under 25 lines (ideally 5-10 lines)
- Each function should do one thing and do it well
- Extract complex operations into separate functions
- Parameters
- Validate parameters in public methods
- Throw exceptions for invalid inputs
- Keep parameter count low (ideally 0-3)
- Use parameter objects for multiple related parameters
- Return Values
- Avoid returning null when possible
- Return empty collections instead of null for collections
- Consider using Optional for potentially missing values
- Try to avoid multiple return statements
- Organization
- Group functions logically
- Separate functions for initialization and business logic
- Keep functions testable (avoid side effects)
Control Flow
- Conditional Statements
- Avoid deep nested if/else statements (max 2-3 levels)
- Use guard clauses for early returns
- Break complex conditions into well-named helper methods or variables
- Use parentheses in long conditions to clarify precedence
- Loops
- Keep loop bodies simple
- Extract complex loop bodies into separate functions
- Consider functional approaches where appropriate
- Be aware of performance implications for large datasets
Error Handling
- Exception Strategy
- Define a clear exception hierarchy
- Don’t suppress exceptions without handling them
- Maintain the original exception’s cause when wrapping
- Document exceptions in method contracts
- Exception Handling
- Do not write complex code in exception handlers
- Extract try/catch blocks to dedicated functions
- Log sufficient information for troubleshooting
- Follow language-specific best practices
Comments and Documentation
- Effective Comments
- Write comments for complex or non-obvious code
- Explain why, not what (the code shows what)
- Keep comments up-to-date when code changes
- Use standardized comment formats (JavaDoc, JSDoc, etc.)
- Error Messages
- Create clear, actionable error messages
- Use error message frameworks for consistency
- Avoid exposing sensitive information in errors
- Include relevant context for troubleshooting
Logging and Monitoring
- Logging Strategy
- Use appropriate log levels (ERROR, WARN, INFO, DEBUG)
- Log contextual information (request IDs, user IDs)
- Follow consistent log patterns
- Avoid logging sensitive information
- Performance Considerations
- Be mindful of logging overhead
- Use conditional logging for verbose information
- Consider log aggregation and search requirements
Clean Code Principles
Code Clarity
- Intention-Revealing Names
- Name variables, methods, and classes to reveal their purpose
- Avoid abbreviations and acronyms unless universally understood
- Use pronounceable names that can be discussed in conversation
- Example:
getUserTransactionHistory() instead of getUsrTrHist()
- Function Design
- Functions should do one thing, do it well, and do it only
- Functions should be small – ideally 5-10 lines
- Reduce function parameters to absolute minimum (0-2 is ideal)
- Functions that modify state should not return values (Command-Query Separation)
- Extract try/catch blocks into their own functions for cleaner code
- Comments
- Good code mostly documents itself; comments should explain “why” not “what”
- Comments that merely echo the code are worse than no comments
- Keep comments relevant and up-to-date or delete them
- Use comments for legal information, explanation of intent, clarification, and warnings
Structure and Organization
- The Scout Rule
- Always leave the code cleaner than you found it
- Make incremental improvements with each touch
- Refactor gradually to avoid breaking changes
- Step-Down Rule
- Organize code to read like a top-down narrative
- Each function should be followed by those at the next level of abstraction
- Creates natural flow from high-level concepts to implementation details
- Cohesion and Coupling
- Classes should be highly cohesive (focused on a single responsibility)
- Minimize coupling between classes and packages
- Law of Demeter: Only talk to your immediate friends (avoid chains like
a.getB().getC().doSomething())
SOLID Principles
- Single Responsibility Principle (SRP)
- A class should have only one reason to change
- Separate business logic from infrastructure concerns
- Example: Separate OrderProcessor from OrderRepository
- Open/Closed Principle (OCP)
- Software entities should be open for extension but closed for modification
- Use abstractions and polymorphism to allow behavior changes without modifying existing code
- Example: Strategy pattern for payment processing methods
- Liskov Substitution Principle (LSP)
- Subtypes must be substitutable for their base types without altering program correctness
- Override methods should not violate parent class contracts
- Example: Square is not a proper subtype of Rectangle if it violates Rectangle’s behavior
- Interface Segregation Principle (ISP)
- Clients should not be forced to depend on methods they do not use
- Create specific interfaces rather than general-purpose ones
- Example: Split large interfaces into
OrderCreator, OrderFinder, etc.
- Dependency Inversion Principle (DIP)
- High-level modules should not depend on low-level modules; both should depend on abstractions
- Abstractions should not depend on details; details should depend on abstractions
- Example: Service depends on Repository interface, not implementation
Clean Architecture
- Independence from Frameworks
- The framework is a tool, not the architecture
- Core business logic should be isolated from framework code
- Use adapters/wrappers to interact with frameworks
- Testability
- Business rules should be testable without UI, database, or external services
- Use dependency injection to allow substituting test doubles
- Create a testing strategy for each architectural layer
- Independence of Delivery Mechanism
- Business logic should not know or care whether it’s being accessed via web, console, or API
- Separate domain models from data transfer objects (DTOs)
- Use mappers to convert between domain and external representations
- Screaming Architecture
- Your architecture should “scream” the intent of the system
- Package structure should reflect business domains, not technical concerns
- Example:
com.company.billing, com.company.shipping instead of com.company.controllers, com.company.services
Layered Architecture
- Layer Separation
- Implement strict layering (e.g., Controller → Service → Repository)
- Each layer has specific responsibilities
- Upper layers call lower layers, never vice versa
- Layer Responsibilities
- Presentation Layer: Handles user interaction and display
- Service Layer: Contains business logic and orchestrates operations
- Data Access Layer: Manages data storage and retrieval
- Domain Layer: Contains business entities and rules
- Package Structure
- Organize by feature or domain, not technical layer
- Follow consistent naming conventions
- Place functions in domain-appropriate packages
- Avoid generic “utils” packages when possible
Java-Specific Guidelines
Core Java Features
- Effective Use of Java Language Features
- Prefer enhanced for loops over traditional loops
- Use annotations for configuration instead of XML when possible
- Use
var judiciously for local variables to reduce verbosity (Java 10+)
- Avoid checked exceptions for predictable cases; use runtime exceptions or Optional
- Use
@Override annotation for all overridden methods
- Enums
- Use enums instead of constants for related groups of values
- Leverage enum methods and fields for related functionality
- Consider using enum-based singletons for type-safe factories
public enum PaymentMethod {
CREDIT_CARD(true) {
@Override
public void process(Payment payment) {
// Process credit card payment
}
},
PAYPAL(true) {
@Override
public void process(Payment payment) {
// Process PayPal payment
}
},
INVOICE(false) {
@Override
public void process(Payment payment) {
// Process invoice payment
}
};
private final boolean isOnline;
PaymentMethod(boolean isOnline) {
this.isOnline = isOnline;
}
public boolean isOnlineMethod() {
return isOnline;
}
public abstract void process(Payment payment);
public static List<PaymentMethod> getOnlineMethods() {
return Arrays.stream(values())
.filter(PaymentMethod::isOnlineMethod)
.collect(Collectors.toList());
}
}
- Generics
- Use generics for type safety and to avoid casts
- Understand PECS: “Producer Extends, Consumer Super”
- Use wildcards appropriately to increase API flexibility
// Producer (read from) - use extends
public void process(List<? extends Animal> animals) {
for (Animal animal : animals) {
// can read animals
}
}
// Consumer (write to) - use super
public void addCats(List<? super Cat> catList) {
catList.add(new Cat()); // can add cats or its subtypes
}
- Default Methods
- Use default methods to evolve interfaces without breaking implementations
- Keep default methods simple, avoiding complex state or dependencies
public interface UserRepository {
List<User> findByRole(Role role);
// Default method to find admins
default List<User> findAdmins() {
return findByRole(Role.ADMIN);
}
}
- Records (Java 16+)
- Use records for simple data carriers
- Leverage compact constructors for validation
- Combine with sealed classes for complete domain modeling
public record OrderDetails(
String orderId,
BigDecimal amount,
LocalDateTime createdAt
) {
// Compact constructor for validation
public OrderDetails {
Objects.requireNonNull(orderId, "Order ID cannot be null");
Objects.requireNonNull(amount, "Amount cannot be null");
Objects.requireNonNull(createdAt, "Created date cannot be null");
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
}
}
- Streams and Lambda Expressions
- Use streams for collection processing to improve readability
- Avoid side effects in stream operations
- Use method references instead of lambdas when possible
- Know when to use parallel streams (CPU-intensive operations on large collections)
- Extract complex stream operations into well-named methods
// Good: Clear, functional style with descriptive method references
public List<UserDto> getActiveUserDtos() {
return userRepository.findAll().stream()
.filter(User::isActive)
.filter(this::hasPermissions)
.map(userMapper::toDto)
.collect(Collectors.toList());
}
// Avoid: Overly complex stream pipeline
public List<UserDto> getActiveUserDtos() {
return userRepository.findAll().stream()
.filter(user -> user.getStatus() == Status.ACTIVE
&& !user.getRoles().isEmpty()
&& user.getLastLogin().isAfter(LocalDate.now().minusDays(30)))
.map(user -> new UserDto(user.getId(), user.getName(), user.getEmail()))
.collect(Collectors.toList());
}
- Optional Usage
- Use Optional as a return type, not a parameter type
- Avoid creating Optional objects for null checks
- Leverage Optional methods like
map(), filter(), and orElse()
- Don’t use
get() without checking isPresent() first; prefer orElse*() methods
// Good: Fluent Optional usage
public String getUserDisplayName(Long userId) {
return userRepository.findById(userId)
.map(User::getDisplayName)
.orElse("Guest");
}
// Avoid: Improper Optional usage
public Optional<User> findUser(Optional<String> username) { // Don't use Optional as parameter
if (username.isPresent()) {
return Optional.ofNullable(userRepository.findByUsername(username.get()));
}
return Optional.empty();
}
Collections & Data Structures
- Collection Selection
- Choose the appropriate collection for your use case:
- ArrayList: Fast random access, slower insertions/deletions
- LinkedList: Fast insertions/deletions, slower random access
- HashSet: Fast lookups, no ordering guarantees
- TreeSet: Sorted elements, slower than HashSet
- HashMap: Fast key lookups
- LinkedHashMap: Predictable iteration order
- TreeMap: Sorted keys
- Collection Factory Methods (Java 9+)
- Use collection factory methods for creating small, immutable collections
// Good: Concise immutable collections
List<String> colors = List.of("red", "green", "blue");
Set<String> primaryColors = Set.of("red", "blue", "yellow");
Map<String, Integer> ratings = Map.of(
"Star Wars", 5,
"Inception", 4,
"Titanic", 3
);
- Collection Performance
- Set initial capacity when you know the approximate size
- Use
EnumMap and EnumSet for enum-based keys for better performance
- Use
ArrayDeque instead of Stack for stack operations
// Good: Improved performance with proper sizing and specialized collections
Map<String, User> userMap = new HashMap<>(1000); // Preallocate for 1000 users
// Use EnumMap for enum keys
Map<UserRole, List<User>> usersByRole = new EnumMap<>(UserRole.class);
// Use ArrayDeque instead of Stack
Deque<Command> commandStack = new ArrayDeque<>();
commandStack.push(command);
Command lastCommand = commandStack.pop();
- Type Safety
- Always use generics when working with collections
- Declare collection variables with interface types:
// Good:
Map<String, User> users = new HashMap<>();
List<Order> orders = new ArrayList<>();
- Avoid raw types
- Avoid using
Object as a parameter or return type
Date and Time
- Modern Date/Time API
- Use
java.time package instead of legacy Date/Calendar
- Choose appropriate classes for your needs:
LocalDate for date without time
LocalTime for time without date
LocalDateTime for date and time without timezone
ZonedDateTime for date and time with timezone
Instant for machine timestamps
- Timezone Handling
- Always specify timezones explicitly when needed
- Store dates in UTC internally
- Convert to user’s timezone only for display
- Use a consistent timezone throughout your application
- Formatting and Parsing
- Use
DateTimeFormatter for formatting and parsing
- Create reusable formatters as constants
- Consider locale when formatting dates for display
// Define reusable formatters
private static final DateTimeFormatter ISO_FORMATTER =
DateTimeFormatter.ISO_DATE_TIME;
private static final DateTimeFormatter DISPLAY_FORMATTER =
DateTimeFormatter.ofPattern("MMM d, yyyy h:mm a", Locale.US);
// Parsing
LocalDateTime dateTime = LocalDateTime.parse(isoString, ISO_FORMATTER);
// Formatting
String displayDate = dateTime.format(DISPLAY_FORMATTER);
String Handling
- Efficient String Operations
- Use
StringBuilder for string concatenation in loops
- Use
String.format() or text blocks (Java 15+) for multi-line strings
- Be careful with string splitting and regular expressions in performance-critical code
- Text Blocks (Java 15+)
- Use text blocks for SQL queries, HTML, JSON, etc.
String sql = """
SELECT u.id, u.name, u.email
FROM users u
JOIN roles r ON u.role_id = r.id
WHERE r.name = ?
ORDER BY u.name
""";
- Character Encoding
- Always specify character encoding when reading/writing text
- Use UTF-8 as the default encoding
- Be explicit about encoding in file I/O and network operations
Spring Framework Practices
- Dependency Injection
- Prefer constructor injection over field or setter injection
- Keep components stateless when possible
- Use qualifiers or named beans to resolve ambiguities
// Good: Constructor injection
@Service
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Autowired // Optional in newer Spring versions
public UserServiceImpl(UserRepository userRepository,
PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
}
// Avoid: Field injection
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
}
- Configuration Management
- Use
@Configuration classes instead of XML
- Group configurations by functional area
- Use profiles for environment-specific configuration
- Externalize sensitive configuration (credentials, API keys)
@Configuration
@PropertySource("classpath:messaging.properties")
public class MessagingConfiguration {
@Bean
@Profile("production")
public MessageSender productionMessageSender() {
return new KafkaMessageSender();
}
@Bean
@Profile("development")
public MessageSender developmentMessageSender() {
return new InMemoryMessageSender();
}
}
- Spring Boot Best Practices
- Use starter dependencies to reduce boilerplate
- Leverage auto-configuration but understand what’s happening
- Override only what you need to customize
- Use application-{profile}.properties for profile-specific configuration
- Prefer configuration properties classes over direct @Value injections
@Configuration
@ConfigurationProperties(prefix = "app.mail")
@Validated
public class MailProperties {
@NotBlank
private String host;
@Min(1)
@Max(65535)
private int port = 25;
private String username;
private String password;
// Getters and setters
}
Database & Persistence
- JPA/Hibernate Best Practices
- Use lazy loading judiciously and handle N+1 query problems
- Define appropriate fetch strategies in queries
- Configure proper cascade types (avoid CascadeType.ALL)
- Use value types for complex attributes without identity
- Set appropriate batch sizes for batch operations
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
@OneToMany(mappedBy = "order", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private List<OrderItem> items = new ArrayList<>();
@Embedded
private Address shippingAddress;
// N+1 problem avoided with fetch join
public static Optional<Order> findByIdWithItems(EntityManager em, Long id) {
return em.createQuery(
"SELECT o FROM Order o " +
"LEFT JOIN FETCH o.items " +
"WHERE o.id = :id", Order.class)
.setParameter("id", id)
.getResultStream()
.findFirst();
}
}
- Transaction Management
- Use declarative transaction management with
@Transactional
- Set the appropriate isolation level for your use case
- Keep transactions as short as possible
- Be explicit about read-only transactions
- Understand transaction propagation behaviors
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final InventoryService inventoryService;
// Constructor omitted for brevity
@Transactional
public Order createOrder(OrderRequest request) {
// This method creates a new transaction
Order order = new Order();
// Set order properties
return orderRepository.save(order);
}
@Transactional(readOnly = true)
public List<Order> findOrdersByCustomer(Long customerId) {
// Read-only optimization
return orderRepository.findByCustomerId(customerId);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void processRefund(Long orderId) {
// Always creates a new transaction
// even if called from another transactional method
}
}
- Native Queries
- Specify the result class when using native queries
- Use named parameters instead of positional parameters
- Consider using projections for partial entity loading
@Repository
public class OrderRepositoryImpl implements OrderRepositoryCustom {
@PersistenceContext
private EntityManager entityManager;
@Override
public List<Order> findActiveOrdersByCustomer(Long customerId) {
return entityManager.createNativeQuery(
"SELECT o.* FROM orders o " +
"WHERE o.customer_id = :customerId " +
"AND o.status = 'ACTIVE'", Order.class)
.setParameter("customerId", customerId)
.getResultList();
}
@Override
public List<OrderSummary> findOrderSummaries() {
return entityManager.createNativeQuery(
"SELECT o.id as orderId, o.order_date as orderDate, " +
"c.name as customerName, SUM(oi.quantity * oi.price) as total " +
"FROM orders o " +
"JOIN customers c ON o.customer_id = c.id " +
"JOIN order_items oi ON oi.order_id = o.id " +
"GROUP BY o.id, o.order_date, c.name",
"OrderSummaryMapping")
.getResultList();
}
}
@Entity
@SqlResultSetMapping(
name = "OrderSummaryMapping",
classes = @ConstructorResult(
targetClass = OrderSummary.class,
columns = {
@ColumnResult(name = "orderId", type = Long.class),
@ColumnResult(name = "orderDate", type = LocalDate.class),
@ColumnResult(name = "customerName", type = String.class),
@ColumnResult(name = "total", type = BigDecimal.class)
}
)
)
public class Order {
// Entity implementation
}
- Database Access
- Use prepared statements to prevent SQL injection
- Use named parameters in queries for readability
- Consider using JPA entity classes when appropriate
Concurrency & Multithreading
- Thread Safety
- Make classes immutable when possible
- Use concurrent collections from
java.util.concurrent
- Understand the proper use of
synchronized, volatile, and atomic classes
- Minimize shared mutable state
- Document thread safety (or lack thereof) in class Javadoc
/**
* Thread-safe counter implementation.
*/
public class Counter {
private final AtomicLong count = new AtomicLong();
public void increment() {
count.incrementAndGet();
}
public long getCount() {
return count.get();
}
}
- Modern Concurrency (Java 8+)
- Use CompletableFuture for async operations
- Understand the Fork/Join framework for parallel tasks
- Use ExecutorService properly and shut it down
- Consider thread pools and task executors for managing threads
@Service
public class ProductEnrichmentService {
private final ExecutorService executor;
private final PriceService priceService;
private final InventoryService inventoryService;
private final ReviewService reviewService;
public ProductEnrichmentService(PriceService priceService,
InventoryService inventoryService,
ReviewService reviewService) {
this.priceService = priceService;
this.inventoryService = inventoryService;
this.reviewService = reviewService;
this.executor = Executors.newFixedThreadPool(10);
}
public CompletableFuture<EnrichedProduct> enrichProduct(Product product) {
CompletableFuture<Price> priceFuture =
CompletableFuture.supplyAsync(() -> priceService.getPrice(product.getId()), executor);
CompletableFuture<InventoryStatus> inventoryFuture =
CompletableFuture.supplyAsync(() -> inventoryService.getStatus(product.getId()), executor);
CompletableFuture<List<Review>> reviewsFuture =
CompletableFuture.supplyAsync(() -> reviewService.getReviews(product.getId()), executor);
return CompletableFuture.allOf(priceFuture, inventoryFuture, reviewsFuture)
.thenApply(v -> new EnrichedProduct(
product,
priceFuture.join(),
inventoryFuture.join(),
reviewsFuture.join()
));
}
@PreDestroy
public void shutdown() {
executor.shutdown();
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
}
}
RESTful API Design
- Resource Modeling
- Design URLs around resources, not actions
- Use appropriate HTTP methods (GET, POST, PUT, DELETE)
- Use proper HTTP status codes
- Provide API versioning strategy
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
private final OrderService orderService;
// Constructor omitted for brevity
@GetMapping
public List<OrderDto> getAllOrders() {
return orderService.findAll();
}
@GetMapping("/{id}")
public ResponseEntity<OrderDto> getOrder(@PathVariable Long id) {
return orderService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public OrderDto createOrder(@Valid @RequestBody OrderRequest request) {
return orderService.createOrder(request);
}
@PutMapping("/{id}")
public ResponseEntity<OrderDto> updateOrder(
@PathVariable Long id,
@Valid @RequestBody OrderRequest request) {
return ResponseEntity.ok(orderService.updateOrder(id, request));
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deleteOrder(@PathVariable Long id) {
orderService.deleteOrder(id);
}
}
- API Documentation
- Use Swagger/OpenAPI for API documentation
- Document all endpoints, parameters, and possible responses
- Include examples for better understanding “`java
@RestController
@RequestMapping("/api/v1/products")
@Tag(name = "Product Management", description = "APIs for managing products")
public class ProductController { @Operation(
summary = "Get a product by ID",
description = "Returns a single product by its unique identifier",
responses = {
@ApiResponse(
responseCode = "200",
description = "Product found",
content = @Content(schema = @Schema(implementation = ProductDto.class))
),
@ApiResponse(
responseCode = "404",
description = "Product not found",
content = @Content(schema = @Schema(implementation = ErrorResponse.class))
)
}