Backend & APIs Topic
1. Layered (N-Tier) Architecture
Detailed Explanation
Layered architecture organizes code into horizontal layers with strict separation of concerns. Each layer has a specific responsibility and can only communicate with adjacent layers. This creates a clean, testable structure that's ideal for many business applications.
Typical Flow:
Controller (Presentation) → Service (Business Logic) → Repository (Data Access) → Database
Code Example: E-commerce Order System
Layer 1: Presentation Layer (Controllers)
// OrderController.java
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping
public ResponseEntity<OrderResponse> createOrder(@RequestBody CreateOrderRequest request) {
OrderResponse order = orderService.createOrder(request);
return ResponseEntity.status(HttpStatus.CREATED).body(order);
}
@GetMapping("/{orderId}")
public ResponseEntity<OrderResponse> getOrder(@PathVariable Long orderId) {
OrderResponse order = orderService.getOrder(orderId);
return ResponseEntity.ok(order);
}
}
// DTOs (Data Transfer Objects) for API contracts
public class CreateOrderRequest {
private Long userId;
private List<OrderItemRequest> items;
private String shippingAddress;
// constructors, getters, setters
}
public class OrderResponse {
private Long orderId;
private String status;
private BigDecimal totalAmount;
private LocalDateTime createdAt;
// constructors, getters, setters
}
Layer 2: Business Logic Layer (Services)
// OrderService.java
@Service
@Transactional
public class OrderService {
private final OrderRepository orderRepository;
private final InventoryService inventoryService;
private final PaymentService paymentService;
public OrderService(OrderRepository orderRepository,
InventoryService inventoryService,
PaymentService paymentService) {
this.orderRepository = orderRepository;
this.inventoryService = inventoryService;
this.paymentService = paymentService;
}
public OrderResponse createOrder(CreateOrderRequest request) {
// 1. Validate business rules
if (request.getItems().isEmpty()) {
throw new IllegalArgumentException("Order must have at least one item");
}
// 2. Check inventory
inventoryService.checkAvailability(request.getItems());
// 3. Calculate total
BigDecimal total = calculateOrderTotal(request.getItems());
// 4. Create order entity
Order order = new Order();
order.setUserId(request.getUserId());
order.setTotalAmount(total);
order.setStatus("PENDING");
order.setShippingAddress(request.getShippingAddress());
// 5. Save to database
Order savedOrder = orderRepository.save(order);
// 6. Process payment
paymentService.processPayment(savedOrder.getId(), total);
// 7. Update inventory
inventoryService.updateStock(request.getItems());
// 8. Return response DTO
return mapToResponse(savedOrder);
}
private BigDecimal calculateOrderTotal(List<OrderItemRequest> items) {
return items.stream()
.map(item -> item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())))
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
private OrderResponse mapToResponse(Order order) {
// mapping logic
}
}
Layer 3: Data Access Layer (Repositories)
// OrderRepository.java
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
List<Order> findByUserIdAndStatus(Long userId, String status);
List<Order> findByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
@Query("SELECT o FROM Order o WHERE o.status = :status AND o.totalAmount > :minAmount")
List<Order> findLargeOrdersByStatus(@Param("status") String status,
@Param("minAmount") BigDecimal minAmount);
}
// Order Entity (JPA)
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long userId;
private BigDecimal totalAmount;
private String status;
private String shippingAddress;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> items;
@CreationTimestamp
private LocalDateTime createdAt;
// constructors, getters, setters
}
Layer 4: Database Layer
-- Database schema
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
total_amount DECIMAL(10,2) NOT NULL,
status VARCHAR(50) NOT NULL,
shipping_address TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE order_items (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL,
price DECIMAL(10,2) NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(id)
);
2. Microservices Architecture
Detailed Explanation
Microservices break down an application into small, independent services that communicate over the network. Each service owns its data and can be developed, deployed, and scaled independently.
Key Characteristics:
-
Single responsibility per service
-
Independent deployment
-
Technology diversity
-
Decentralized data management
Code Example: E-commerce Platform
Service 1: Order Service
// OrderService Application
@SpringBootApplication
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
// Order Controller
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService orderService;
@PostMapping
public ResponseEntity<OrderDTO> createOrder(@RequestBody CreateOrderDTO orderDTO) {
OrderDTO order = orderService.createOrder(orderDTO);
return ResponseEntity.status(HttpStatus.CREATED).body(order);
}
}
// Order Service
@Service
@Transactional
public class OrderService {
private final OrderRepository orderRepository;
private final InventoryServiceClient inventoryServiceClient;
private final PaymentServiceClient paymentServiceClient;
public OrderDTO createOrder(CreateOrderDTO orderDTO) {
// 1. Call Inventory Service to check stock
InventoryResponse inventoryResponse = inventoryServiceClient.checkInventory(
orderDTO.getItems()
);
if (!inventoryResponse.isAvailable()) {
throw new InsufficientInventoryException("Items not available");
}
// 2. Create order
Order order = mapToEntity(orderDTO);
order.setStatus("CREATED");
Order savedOrder = orderRepository.save(order);
// 3. Call Payment Service
PaymentRequest paymentRequest = new PaymentRequest(
savedOrder.getId(),
savedOrder.getTotalAmount(),
orderDTO.getPaymentMethod()
);
PaymentResponse paymentResponse = paymentServiceClient.processPayment(paymentRequest);
// 4. Update order status based on payment
if (paymentResponse.isSuccess()) {
savedOrder.setStatus("CONFIRMED");
orderRepository.save(savedOrder);
// 5. Call Inventory Service to update stock
inventoryServiceClient.updateInventory(orderDTO.getItems());
}
return mapToDTO(savedOrder);
}
}
// Feign Client for service-to-service communication
@FeignClient(name = "inventory-service", url = "${services.inventory.url}")
public interface InventoryServiceClient {
@PostMapping("/inventory/check")
InventoryResponse checkInventory(@RequestBody List<OrderItemDTO> items);
@PostMapping("/inventory/update")
void updateInventory(@RequestBody List<OrderItemDTO> items);
}
// Order Service's own database
@Entity
@Table(name = "orders")
public class Order {
@Id
private String id; // UUID
private String userId;
private BigDecimal totalAmount;
private String status;
private LocalDateTime createdAt;
// order-specific fields
}
Service 2: Inventory Service
// Inventory Service Application
@SpringBootApplication
public class InventoryServiceApplication {
public static void main(String[] args) {
SpringApplication.run(InventoryServiceApplication.class, args);
}
}
// Inventory Controller
@RestController
@RequestMapping("/inventory")
public class InventoryController {
private final InventoryService inventoryService;
@PostMapping("/check")
public InventoryResponse checkInventory(@RequestBody List<OrderItemDTO> items) {
return inventoryService.checkAvailability(items);
}
@PostMapping("/update")
public void updateInventory(@RequestBody List<OrderItemDTO> items) {
inventoryService.updateStock(items);
}
}
// Inventory Service
@Service
public class InventoryService {
private final ProductRepository productRepository;
public InventoryResponse checkAvailability(List<OrderItemDTO> items) {
for (OrderItemDTO item : items) {
Product product = productRepository.findById(item.getProductId())
.orElseThrow(() -> new ProductNotFoundException(item.getProductId()));
if (product.getStockQuantity() < item.getQuantity()) {
return new InventoryResponse(false,
"Insufficient stock for product: " + product.getName());
}
}
return new InventoryResponse(true, "All items available");
}
}
// Inventory Service's own database schema
@Entity
@Table(name = "products")
public class Product {
@Id
private Long id;
private String name;
private String description;
private Integer stockQuantity;
private BigDecimal price;
// inventory-specific fields
}
Service 3: API Gateway
# Spring Cloud Gateway configuration
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
- id: inventory-service
uri: lb://inventory-service
predicates:
- Path=/api/inventory/**
- id: payment-service
uri: lb://payment-service
predicates:
- Path=/api/payment/**
3. Event-Driven Architecture
Detailed Explanation
Event-driven architecture uses events to trigger and communicate between decoupled services. When something significant happens, a service produces an event, and other services consume and react to these events.
Key Components:
-
Event Producers: Services that publish events
-
Event Consumers: Services that subscribe to events
-
Event Bus/Message Broker: Middleware that routes events
Code Example: Order Processing System
Event Definitions
java
// Base Event
public abstract class DomainEvent {
private String eventId;
private String aggregateId; // e.g., orderId
private LocalDateTime timestamp;
private String eventType;
public DomainEvent(String aggregateId) {
this.eventId = UUID.randomUUID().toString();
this.aggregateId = aggregateId;
this.timestamp = LocalDateTime.now();
this.eventType = this.getClass().getSimpleName();
}
// getters
}
// Specific Events
public class OrderCreatedEvent extends DomainEvent {
private Long userId;
private BigDecimal totalAmount;
private List<OrderItem> items;
public OrderCreatedEvent(String orderId, Long userId,
BigDecimal totalAmount, List<OrderItem> items) {
super(orderId);
this.userId = userId;
this.totalAmount = totalAmount;
this.items = items;
}
// getters
}
public class PaymentProcessedEvent extends DomainEvent {
private boolean success;
private String paymentId;
private String failureReason;
public PaymentProcessedEvent(String orderId, boolean success,
String paymentId, String failureReason) {
super(orderId);
this.success = success;
this.paymentId = paymentId;
this.failureReason = failureReason;
}
// getters
}
public class InventoryUpdatedEvent extends DomainEvent {
private List<OrderItem> items;
public InventoryUpdatedEvent(String orderId, List<OrderItem> items) {
super(orderId);
this.items = items;
}
// getters
}
Event Producer (Order Service)
java
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final EventPublisher eventPublisher;
@Transactional
public OrderDTO createOrder(CreateOrderDTO orderDTO) {
// 1. Create order
Order order = mapToEntity(orderDTO);
order.setStatus("CREATED");
Order savedOrder = orderRepository.save(order);
// 2. Publish OrderCreatedEvent
OrderCreatedEvent event = new OrderCreatedEvent(
savedOrder.getId().toString(),
savedOrder.getUserId(),
savedOrder.getTotalAmount(),
savedOrder.getItems()
);
eventPublisher.publishOrderCreated(event);
return mapToDTO(savedOrder);
}
}
// Event Publisher using Spring AMQP (RabbitMQ)
@Component
public class EventPublisher {
private final AmqpTemplate amqpTemplate;
public EventPublisher(AmqpTemplate amqpTemplate) {
this.amqpTemplate = amqpTemplate;
}
public void publishOrderCreated(OrderCreatedEvent event) {
amqpTemplate.convertAndSend("order-events-exchange",
"order.created",
event);
}
public void publishPaymentProcessed(PaymentProcessedEvent event) {
amqpTemplate.convertAndSend("payment-events-exchange",
"payment.processed",
event);
}
}
Event Consumers
Payment Service Consumer
java
@Service
public class PaymentEventConsumer {
private final PaymentService paymentService;
// Listen for OrderCreatedEvent to process payment
@RabbitListener(queues = "payment.process.queue")
public void handleOrderCreated(OrderCreatedEvent event) {
try {
// Process payment for the order
PaymentResult result = paymentService.processPayment(
event.getAggregateId(), // orderId
event.getTotalAmount()
);
// Publish PaymentProcessedEvent
PaymentProcessedEvent paymentEvent = new PaymentProcessedEvent(
event.getAggregateId(),
result.isSuccess(),
result.getPaymentId(),
result.getFailureReason()
);
// Send response back to event bus
eventPublisher.publishPaymentProcessed(paymentEvent);
} catch (Exception e) {
// Handle failure - might go to dead letter queue
log.error("Failed to process payment for order: {}", event.getAggregateId(), e);
}
}
}
Inventory Service Consumer
java
@Service
public class InventoryEventConsumer {
private final InventoryService inventoryService;
// Listen for OrderCreatedEvent to reserve inventory
@RabbitListener(queues = "inventory.reserve.queue")
public void handleOrderCreated(OrderCreatedEvent event) {
inventoryService.reserveItems(event.getItems());
}
// Listen for PaymentProcessedEvent to update inventory
@RabbitListener(queues = "inventory.update.queue")
public void handlePaymentProcessed(PaymentProcessedEvent event) {
if (event.isSuccess()) {
// Payment succeeded, permanently deduct inventory
inventoryService.confirmReservation(event.getAggregateId());
} else {
// Payment failed, release reserved inventory
inventoryService.releaseReservation(event.getAggregateId());
}
}
}
Notification Service Consumer
java
@Service
public class NotificationEventConsumer {
private final EmailService emailService;
private final NotificationService notificationService;
// Listen for various events to send notifications
@RabbitListener(queues = "notification.queue")
public void handleOrderEvents(DomainEvent event) {
switch (event.getClass().getSimpleName()) {
case "OrderCreatedEvent":
notificationService.sendOrderConfirmation(event.getAggregateId());
break;
case "PaymentProcessedEvent":
PaymentProcessedEvent paymentEvent = (PaymentProcessedEvent) event;
if (paymentEvent.isSuccess()) {
notificationService.sendPaymentSuccess(event.getAggregateId());
} else {
notificationService.sendPaymentFailure(event.getAggregateId(),
paymentEvent.getFailureReason());
}
break;
case "InventoryUpdatedEvent":
notificationService.sendShippingUpdate(event.getAggregateId());
break;
}
}
}
Message Broker Configuration
yaml
# application.yml for RabbitMQ configuration
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
# Exchange and Queue configuration
@Configuration
public class EventConfiguration {
@Bean
public TopicExchange orderEventsExchange() {
return new TopicExchange("order-events-exchange");
}
@Bean
public Queue paymentProcessingQueue() {
return new Queue("payment.process.queue", true);
}
@Bean
public Queue inventoryReservationQueue() {
return new Queue("inventory.reserve.queue", true);
}
@Bean
public Queue notificationQueue() {
return new Queue("notification.queue", true);
}
@Bean
public Binding paymentBinding() {
return BindingBuilder.bind(paymentProcessingQueue())
.to(orderEventsExchange())
.with("order.created");
}
@Bean
public Binding inventoryBinding() {
return BindingBuilder.bind(inventoryReservationQueue())
.to(orderEventsExchange())
.with("order.created");
}
}
When to Use Each Patter:
-
Layered Architecture: Startups, simple CRUD apps, when you need quick development and have a small team
-
Microservices: Large scale systems, multiple teams, need for independent scaling and deployment
-
Event-Driven: Real-time systems, complex workflows, when you need loose coupling and async processing
Each pattern solves different problems and the choice depends on your specific requirements around scalability, team structure, and complexity tolerance.
Comments (0)
No comments yet. Be the first to comment!