Software Development Principles¶
Table of Contents¶
SOLID Principles¶
Overview¶
SOLID is an acronym for five design principles intended to make object-oriented designs more understandable, flexible, and maintainable.
1. Single Responsibility Principle (SRP)¶
A class should have only one reason to change.
// Bad Example
class UserManager {
public void saveUser(User user) {
// Save user to database
}
public void sendEmail(User user) {
// Send email to user
}
}
// Good Example
class UserRepository {
public void saveUser(User user) {
// Save user to database
}
}
class EmailService {
public void sendEmail(User user) {
// Send email to user
}
}
2. Open/Closed Principle (OCP)¶
Software entities should be open for extension but closed for modification.
// Bad Example
class Rectangle {
public double width;
public double height;
}
class AreaCalculator {
public double calculateArea(Rectangle rectangle) {
return rectangle.width * rectangle.height;
}
// Need to modify this class for each new shape
}
// Good Example
interface Shape {
double calculateArea();
}
class Rectangle implements Shape {
private double width;
private double height;
@Override
public double calculateArea() {
return width * height;
}
}
class Circle implements Shape {
private double radius;
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
3. Liskov Substitution Principle (LSP)¶
Objects of a superclass should be replaceable with objects of its subclasses without breaking the application.
// Bad Example
class Bird {
public void fly() {
// Flying implementation
}
}
class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins can't fly");
}
}
// Good Example
interface FlyingBird {
void fly();
}
interface SwimmingBird {
void swim();
}
class Sparrow implements FlyingBird {
@Override
public void fly() {
// Flying implementation
}
}
class Penguin implements SwimmingBird {
@Override
public void swim() {
// Swimming implementation
}
}
4. Interface Segregation Principle (ISP)¶
Clients should not be forced to depend on interfaces they do not use.
// Bad Example
interface Worker {
void work();
void eat();
void sleep();
}
// Good Example
interface Workable {
void work();
}
interface Eatable {
void eat();
}
interface Sleepable {
void sleep();
}
class Human implements Workable, Eatable, Sleepable {
@Override
public void work() {
// Working implementation
}
@Override
public void eat() {
// Eating implementation
}
@Override
public void sleep() {
// Sleeping implementation
}
}
5. Dependency Inversion Principle (DIP)¶
High-level modules should not depend on low-level modules. Both should depend on abstractions.
// Bad Example
class EmailNotifier {
public void sendEmail(String message) {
// Send email implementation
}
}
class NotificationService {
private EmailNotifier emailNotifier = new EmailNotifier();
public void notify(String message) {
emailNotifier.sendEmail(message);
}
}
// Good Example
interface NotificationSender {
void send(String message);
}
class EmailNotifier implements NotificationSender {
@Override
public void send(String message) {
// Send email implementation
}
}
class SMSNotifier implements NotificationSender {
@Override
public void send(String message) {
// Send SMS implementation
}
}
class NotificationService {
private NotificationSender notificationSender;
public NotificationService(NotificationSender notificationSender) {
this.notificationSender = notificationSender;
}
public void notify(String message) {
notificationSender.send(message);
}
}
ACID Properties¶
Overview¶
ACID is an acronym that stands for Atomicity, Consistency, Isolation, and Durability. These properties ensure reliable processing of database transactions.
1. Atomicity¶
A transaction must be treated as a single, indivisible unit of work. Either all operations complete successfully or none of them do.
@Transactional
public void transferMoney(Account from, Account to, BigDecimal amount) {
try {
from.withdraw(amount);
to.deposit(amount);
} catch (Exception e) {
// Transaction will be rolled back automatically
throw new TransactionException("Transfer failed", e);
}
}
2. Consistency¶
A transaction must bring the database from one valid state to another valid state, maintaining all invariants.
@Entity
public class BankAccount {
@Id
private Long id;
private BigDecimal balance;
@PreUpdate
public void validateBalance() {
if (balance.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalStateException("Balance cannot be negative");
}
}
}
3. Isolation¶
Concurrent transactions must not affect each other. Each transaction should appear to execute in isolation.
@Transactional(isolation = Isolation.SERIALIZABLE)
public void updateBalance(Long accountId, BigDecimal amount) {
Account account = accountRepository.findById(accountId)
.orElseThrow(() -> new AccountNotFoundException(accountId));
account.setBalance(account.getBalance().add(amount));
accountRepository.save(account);
}
4. Durability¶
Once a transaction is committed, its changes must persist even in the case of system failures.
@Service
public class TransactionService {
@Autowired
private TransactionRepository transactionRepository;
@Transactional
public void processTransaction(Transaction transaction) {
// Process transaction
transactionRepository.save(transaction);
// After successful commit, changes are permanent
}
}
12-Factor Methodology¶
Overview¶
The 12-Factor methodology is a set of best practices for building modern, scalable, maintainable software-as-a-service applications.
1. Codebase¶
One codebase tracked in revision control, many deploys.
// Example project structure
my-application/
├── .git/
├── src/
├── config/
├── Dockerfile
└── README.md
2. Dependencies¶
Explicitly declare and isolate dependencies.
<!-- pom.xml -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.0</version>
</dependency>
</dependencies>
3. Config¶
Store config in the environment.
@Configuration
public class AppConfig {
@Value("${DATABASE_URL}")
private String databaseUrl;
@Value("${API_KEY}")
private String apiKey;
}
4. Backing Services¶
Treat backing services as attached resources.
@Configuration
public class DatabaseConfig {
@Bean
public DataSource dataSource() {
return DataSourceBuilder.create()
.url(System.getenv("DATABASE_URL"))
.username(System.getenv("DATABASE_USER"))
.password(System.getenv("DATABASE_PASSWORD"))
.build();
}
}
5. Build, Release, Run¶
Strictly separate build and run stages.
# Dockerfile
FROM maven:3.8.4-openjdk-17 AS build
COPY . .
RUN mvn clean package
FROM openjdk:17-jdk-slim
COPY --from=build /target/app.jar app.jar
CMD ["java", "-jar", "app.jar"]
6. Processes¶
Execute the app as one or more stateless processes.
@RestController
@SessionAttributes // Avoid using session state
public class StatelessController {
@GetMapping("/api/data")
public ResponseEntity<?> getData(@RequestParam String id) {
// Process request without storing state
return ResponseEntity.ok(processData(id));
}
}
7. Port Binding¶
Export services via port binding.
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// application.properties
server.port=${PORT:8080}
8. Concurrency¶
Scale out via the process model.
@EnableAsync
@Configuration
public class AsyncConfig {
@Bean
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(500);
return executor;
}
}
9. Disposability¶
Maximize robustness with fast startup and graceful shutdown.
@Component
public class GracefulShutdown {
@PreDestroy
public void onShutdown() {
// Clean up resources
// Close connections
// Finish processing
}
}
10. Dev/Prod Parity¶
Keep development, staging, and production as similar as possible.
@Profile("dev")
@Configuration
public class DevConfig {
// Development-specific configuration
}
@Profile("prod")
@Configuration
public class ProdConfig {
// Production-specific configuration
}
11. Logs¶
Treat logs as event streams.
@Slf4j
@Service
public class LoggingService {
public void logEvent(String event) {
log.info("Event occurred: {}", event);
// Logs are written to stdout/stderr
}
}
12. Admin Processes¶
Run admin/management tasks as one-off processes.
@Component
public class DatabaseMigration {
@Scheduled(fixedRate = 86400000) // Run once per day
public void performMigration() {
// Execute database migration
}
}
Real-World Scenarios¶
SOLID Principles in Action¶
1. Single Responsibility Principle (SRP)¶
Scenario: E-commerce Order Processing
// Bad Design: OrderProcessor handling multiple responsibilities
public class OrderProcessor {
public void processOrder(Order order) {
// Validate order
validateOrder(order);
// Calculate total
calculateTotal(order);
// Process payment
processPayment(order);
// Update inventory
updateInventory(order);
// Send confirmation email
sendConfirmationEmail(order);
// Generate invoice
generateInvoice(order);
}
}
// Good Design: Separate classes for each responsibility
public class OrderValidator {
public void validateOrder(Order order) {
// Validate order details, stock availability, etc.
}
}
public class PaymentProcessor {
public void processPayment(Order order) {
// Handle payment processing
}
}
public class InventoryManager {
public void updateInventory(Order order) {
// Update product inventory
}
}
public class EmailService {
public void sendConfirmationEmail(Order order) {
// Send order confirmation
}
}
public class InvoiceGenerator {
public void generateInvoice(Order order) {
// Generate invoice
}
}
// Orchestrator
public class OrderService {
private final OrderValidator validator;
private final PaymentProcessor paymentProcessor;
private final InventoryManager inventoryManager;
private final EmailService emailService;
private final InvoiceGenerator invoiceGenerator;
public void processOrder(Order order) {
validator.validateOrder(order);
paymentProcessor.processPayment(order);
inventoryManager.updateInventory(order);
emailService.sendConfirmationEmail(order);
invoiceGenerator.generateInvoice(order);
}
}
2. Open/Closed Principle (OCP)¶
Scenario: Payment Gateway Integration
// Bad Design: Modifying existing code for new payment methods
public class PaymentProcessor {
public void processPayment(String type, double amount) {
if (type.equals("CREDIT_CARD")) {
processCreditCard(amount);
} else if (type.equals("PAYPAL")) {
processPayPal(amount);
}
// Need to modify this class for each new payment method
}
}
// Good Design: Extensible payment processing
public interface PaymentMethod {
void processPayment(double amount);
}
public class CreditCardPayment implements PaymentMethod {
@Override
public void processPayment(double amount) {
// Process credit card payment
}
}
public class PayPalPayment implements PaymentMethod {
@Override
public void processPayment(double amount) {
// Process PayPal payment
}
}
public class CryptoPayment implements PaymentMethod {
@Override
public void processPayment(double amount) {
// Process cryptocurrency payment
}
}
public class PaymentProcessor {
public void processPayment(PaymentMethod paymentMethod, double amount) {
paymentMethod.processPayment(amount);
}
}
3. Liskov Substitution Principle (LSP)¶
Scenario: Document Processing System
// Bad Design: Breaking LSP
public abstract class Document {
abstract void save();
abstract void print();
}
public class PDFDocument extends Document {
@Override
void save() {
// Save PDF
}
@Override
void print() {
// Print PDF
}
}
public class ReadOnlyDocument extends Document {
@Override
void save() {
throw new UnsupportedOperationException("Cannot save read-only document");
}
@Override
void print() {
// Print document
}
}
// Good Design: Proper abstraction
public interface Printable {
void print();
}
public interface Saveable {
void save();
}
public class PDFDocument implements Printable, Saveable {
@Override
public void print() {
// Print PDF
}
@Override
public void save() {
// Save PDF
}
}
public class ReadOnlyDocument implements Printable {
@Override
public void print() {
// Print document
}
}
ACID Properties in Action¶
Real-World Banking Scenario¶
@Service
@Transactional
public class BankingService {
@Autowired
private AccountRepository accountRepository;
@Autowired
private TransactionRepository transactionRepository;
// Atomicity Example: Fund Transfer
public void transferFunds(Long fromAccountId, Long toAccountId, BigDecimal amount) {
Account fromAccount = accountRepository.findById(fromAccountId)
.orElseThrow(() -> new AccountNotFoundException(fromAccountId));
Account toAccount = accountRepository.findById(toAccountId)
.orElseThrow(() -> new AccountNotFoundException(toAccountId));
// Both operations must succeed or both must fail
fromAccount.withdraw(amount);
toAccount.deposit(amount);
accountRepository.save(fromAccount);
accountRepository.save(toAccount);
// Record transaction
Transaction transaction = new Transaction(fromAccount, toAccount, amount);
transactionRepository.save(transaction);
}
// Consistency Example: Account Balance Check
@PreUpdate
@PrePersist
public void validateAccountState(Account account) {
if (account.getBalance().compareTo(BigDecimal.ZERO) < 0 &&
!account.isOverdraftAllowed()) {
throw new InsufficientFundsException("Negative balance not allowed");
}
}
// Isolation Example: Concurrent Withdrawals
@Transactional(isolation = Isolation.SERIALIZABLE)
public void processLargeWithdrawal(Long accountId, BigDecimal amount) {
Account account = accountRepository.findById(accountId)
.orElseThrow(() -> new AccountNotFoundException(accountId));
if (account.getBalance().compareTo(amount) < 0) {
throw new InsufficientFundsException("Insufficient funds");
}
account.withdraw(amount);
accountRepository.save(account);
}
// Durability Example: Transaction Logging
public void logTransaction(Transaction transaction) {
// First, persist to database
transactionRepository.save(transaction);
// Then, write to audit log
auditLogger.logTransaction(transaction);
// Finally, notify monitoring system
monitoringService.notifyTransactionProcessed(transaction);
}
}
12-Factor Methodology in Action¶
Real-World Microservice Example¶
// 1. Codebase - Version-controlled microservice
@SpringBootApplication
public class PaymentServiceApplication {
public static void main(String[] args) {
SpringApplication.run(PaymentServiceApplication.class, args);
}
}
// 2. Dependencies - Explicit dependencies in build file
// build.gradle
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web:2.7.0'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa:2.7.0'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client:3.1.0'
}
// 3. Config - Environment-based configuration
@Configuration
public class PaymentConfig {
@Value("${STRIPE_API_KEY}")
private String stripeApiKey;
@Value("${PAYMENT_PROCESSING_TIMEOUT}")
private int processingTimeout;
@Bean
public Stripe stripeClient() {
return new Stripe(stripeApiKey);
}
}
// 4. Backing Services - Database and external service configuration
@Configuration
public class DatabaseConfig {
@Bean
public DataSource dataSource() {
return DataSourceBuilder.create()
.url(System.getenv("DATABASE_URL"))
.username(System.getenv("DATABASE_USER"))
.password(System.getenv("DATABASE_PASSWORD"))
.build();
}
}
// 5. Build, Release, Run - Containerized deployment
// Dockerfile
FROM gradle:7.4.2-jdk17 AS build
COPY --chown=gradle:gradle . /home/gradle/src
WORKDIR /home/gradle/src
RUN gradle build --no-daemon
FROM openjdk:17-slim
COPY --from=build /home/gradle/src/build/libs/*.jar app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
// 6. Processes - Stateless service
@RestController
@RequestMapping("/api/payments")
public class PaymentController {
@Autowired
private PaymentService paymentService;
@PostMapping
public ResponseEntity<PaymentResponse> processPayment(
@RequestBody PaymentRequest request) {
return ResponseEntity.ok(paymentService.processPayment(request));
}
}
// 7. Port Binding - Self-contained service
server.port=${PORT:8080}
spring.application.name=payment-service
// 8. Concurrency - Scalable processing
@Service
public class PaymentProcessor {
@Async
public CompletableFuture<PaymentResult> processPaymentAsync(
Payment payment) {
return CompletableFuture.supplyAsync(() -> {
// Process payment
return new PaymentResult();
});
}
}
// 9. Disposability - Graceful shutdown
@Component
public class PaymentServiceShutdown {
@PreDestroy
public void onShutdown() {
// Complete pending transactions
// Close database connections
// Clean up resources
}
}
// 10. Dev/Prod Parity - Environment-specific configurations
@Configuration
@Profile("dev")
public class DevConfig {
@Bean
public PaymentGateway paymentGateway() {
return new MockPaymentGateway();
}
}
@Configuration
@Profile("prod")
public class ProdConfig {
@Bean
public PaymentGateway paymentGateway() {
return new StripePaymentGateway();
}
}
// 11. Logs - Centralized logging
@Aspect
@Component
public class PaymentLogging {
private static final Logger logger =
LoggerFactory.getLogger(PaymentLogging.class);
@Around("execution(* com.example.payment.service.*.*(..))")
public Object logPaymentOperation(ProceedingJoinPoint joinPoint)
throws Throwable {
logger.info("Payment operation started: {}",
joinPoint.getSignature().getName());
try {
Object result = joinPoint.proceed();
logger.info("Payment operation completed successfully");
return result;
} catch (Exception e) {
logger.error("Payment operation failed", e);
throw e;
}
}
}
// 12. Admin Processes - Maintenance tasks
@Component
public class PaymentMaintenanceTasks {
@Scheduled(cron = "0 0 2 * * *") // Run at 2 AM daily
public void cleanupFailedPayments() {
// Clean up failed payments
}
@Scheduled(cron = "0 0 0 * * 0") // Run weekly
public void generatePaymentReports() {
// Generate weekly payment reports
}
}
Best Practices Summary¶
- Follow SOLID principles for maintainable object-oriented design
- Ensure ACID properties in database transactions
- Apply 12-factor methodology for modern application development
- Use appropriate tools and frameworks that support these principles
- Regularly review and refactor code to maintain adherence to these principles
- Document decisions and trade-offs when deviating from these principles
- Train team members on these principles and their practical applications