Menu

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!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?