J2EE Design Patterns¶
Overview¶
J2EE (Java 2 Platform, Enterprise Edition) design patterns address common challenges in enterprise application development. These patterns provide solutions for developing robust, scalable, and maintainable enterprise applications. This guide covers essential J2EE design patterns, their implementation, use cases, advantages, and potential drawbacks in the context of modern enterprise Java development.
Prerequisites¶
- Solid understanding of Java programming
- Familiarity with enterprise application concepts
- Knowledge of servlets, JSP, and EJB technologies
- Basic understanding of web application architecture
- Experience with Spring or Jakarta EE frameworks
Learning Objectives¶
- Understand the purpose and benefits of J2EE design patterns
- Learn when and how to implement different J2EE patterns
- Recognize appropriate use cases for each pattern
- Implement J2EE patterns in enterprise applications
- Understand the trade-offs between different J2EE patterns
- Apply best practices when implementing J2EE patterns
Table of Contents¶
- Introduction to J2EE Patterns
- MVC Pattern
- Business Delegate Pattern
- Service Locator Pattern
- Session Facade Pattern
- Data Access Object Pattern
- Front Controller Pattern
- Intercepting Filter Pattern
- Transfer Object Pattern
- Composite Entity Pattern
- Best Practices
- Common Pitfalls
- Modern Alternatives
Introduction to J2EE Patterns¶
J2EE design patterns are used to solve common problems in enterprise Java applications. These patterns can be categorized into three main types: - Presentation tier patterns - Business tier patterns - Integration tier patterns
Why Use J2EE Patterns?¶
- Scalability: They help create applications that can scale to handle enterprise workloads.
- Maintainability: They promote clean code organization and separation of concerns.
- Reusability: They encourage reuse of proven solutions to common problems.
- Flexibility: They allow for easier system evolution and adaptation to changing requirements.
When to Use J2EE Patterns¶
- When building enterprise-scale applications
- When you need to ensure separation of concerns
- When you want to optimize performance in a distributed environment
- When you need to simplify complex enterprise architectures
- When you want to follow industry best practices in enterprise development
MVC Pattern¶
Model-View-Controller (MVC) is a fundamental pattern for organizing code in web applications, separating the application into three interconnected components.
Intent¶
- Separate the application into three main components: Model, View, and Controller
- Ensure a clean separation of concerns
- Improve code organization, reusability, and maintainability
- Allow parallel development of different application aspects
Implementation¶
// Model - represents the data
public class UserModel {
private String username;
private String email;
public UserModel(String username, String email) {
this.username = username;
this.email = email;
}
// Getters and setters
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
// View - displays the data
public class UserView {
public void displayUserDetails(String username, String email) {
System.out.println("User Details:");
System.out.println("Username: " + username);
System.out.println("Email: " + email);
}
}
// Controller - handles the user input and updates model and view
public class UserController {
private UserModel model;
private UserView view;
public UserController(UserModel model, UserView view) {
this.model = model;
this.view = view;
}
public void setUsername(String username) {
model.setUsername(username);
}
public String getUsername() {
return model.getUsername();
}
public void setEmail(String email) {
model.setEmail(email);
}
public String getEmail() {
return model.getEmail();
}
public void updateView() {
view.displayUserDetails(model.getUsername(), model.getEmail());
}
}
// Client code
UserModel model = new UserModel("john_doe", "john@example.com");
UserView view = new UserView();
UserController controller = new UserController(model, view);
controller.updateView(); // Display initial user details
// Update model data through controller
controller.setUsername("jane_doe");
controller.setEmail("jane@example.com");
controller.updateView(); // Display updated user details
Spring MVC Implementation¶
// Model
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String email;
// Getters and setters
}
// Controller
@Controller
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public String getUser(@PathVariable Long id, Model model) {
User user = userService.findById(id);
model.addAttribute("user", user);
return "user-details"; // View name
}
@PostMapping
public String createUser(@ModelAttribute User user) {
userService.save(user);
return "redirect:/users";
}
}
// View (Thymeleaf template)
// user-details.html
// <!DOCTYPE html>
// <html xmlns:th="http://www.thymeleaf.org">
// <head>
// <title>User Details</title>
// </head>
// <body>
// <h1>User Details</h1>
// <p th:text="'Username: ' + ${user.username}"></p>
// <p th:text="'Email: ' + ${user.email}"></p>
// </body>
// </html>
When to Use the MVC Pattern¶
- When building web applications that need clear separation of concerns
- When multiple developers need to work on different aspects of the application
- When you want to improve code organization and maintainability
- When you need to support multiple views of the same data
- When using frameworks that implement MVC (Spring MVC, Struts, etc.)
Advantages¶
- Separation of concerns leads to more maintainable code
- Makes it easier to test individual components
- Allows parallel development of model, view, and controller
- Supports multiple views for the same model
- Follows established best practices for web application design
Disadvantages¶
- Can add complexity for smaller applications
- May require more code than simpler designs
- Can lead to tight coupling if not implemented correctly
- Navigation flow can be complex in large applications
- Learning curve for developers new to the pattern
Business Delegate Pattern¶
The Business Delegate pattern decouples presentation and business tiers, hiding the implementation details of business services.
Intent¶
- Reduce coupling between presentation-tier clients and business services
- Hide the implementation details of business services including lookup and access details
- Translate network exceptions into business exceptions
- Provide a client-side abstraction for remote business services
Implementation¶
// Service interface
public interface OrderService {
void placeOrder(Order order);
Order getOrder(String orderId);
List<Order> getAllOrders();
}
// Service implementation
public class OrderServiceImpl implements OrderService {
@Override
public void placeOrder(Order order) {
System.out.println("Order placed: " + order.getOrderId());
// Implementation details
}
@Override
public Order getOrder(String orderId) {
System.out.println("Fetching order: " + orderId);
// Implementation details
return new Order(orderId);
}
@Override
public List<Order> getAllOrders() {
System.out.println("Fetching all orders");
// Implementation details
return new ArrayList<>();
}
}
// Business delegate
public class OrderBusinessDelegate {
private OrderService orderService;
public OrderBusinessDelegate() {
// Could use a service locator here
this.orderService = new OrderServiceImpl();
}
public void placeOrder(Order order) {
try {
orderService.placeOrder(order);
} catch (Exception e) {
// Translate to business exception
throw new OrderProcessingException("Failed to place order", e);
}
}
public Order getOrder(String orderId) {
try {
return orderService.getOrder(orderId);
} catch (Exception e) {
// Translate to business exception
throw new OrderProcessingException("Failed to fetch order", e);
}
}
public List<Order> getAllOrders() {
try {
return orderService.getAllOrders();
} catch (Exception e) {
// Translate to business exception
throw new OrderProcessingException("Failed to fetch orders", e);
}
}
}
// Order class
public class Order {
private String orderId;
private String customerName;
private double amount;
public Order(String orderId) {
this.orderId = orderId;
}
// Getters and setters
public String getOrderId() {
return orderId;
}
// Additional getters and setters
}
// Business exception
public class OrderProcessingException extends RuntimeException {
public OrderProcessingException(String message, Throwable cause) {
super(message, cause);
}
}
// Client code
OrderBusinessDelegate delegate = new OrderBusinessDelegate();
Order order = new Order("ORD-001");
delegate.placeOrder(order);
Order fetchedOrder = delegate.getOrder("ORD-001");
List<Order> allOrders = delegate.getAllOrders();
When to Use the Business Delegate Pattern¶
- When you need to decouple presentation tier from business services
- When you want to hide the complexity of remote service lookup and access
- When you need to translate system exceptions into business exceptions
- When building enterprise applications with multiple tiers
- When you want to minimize the impact of service API changes on client code
Advantages¶
- Reduces coupling between presentation and business tiers
- Hides service implementation details from clients
- Provides a simpler API for presentation-tier components
- Centralizes error handling and translation
- Makes the client code more robust against service changes
Disadvantages¶
- Adds an extra layer to the architecture
- Can introduce unnecessary complexity in simpler applications
- May become a bottleneck if not designed correctly
- Additional maintenance overhead
- May hide too many details from the client
Service Locator Pattern¶
The Service Locator pattern provides a centralized registry for services, eliminating the need for clients to have direct knowledge of how to obtain service references.
Intent¶
- Encapsulate the processes involved in obtaining a service with a strong abstraction layer
- Centralize service object lookups to facilitate decoupling
- Provide a simple interface for clients to obtain various services
- Cache services for better performance
Implementation¶
// Service interface
public interface MessagingService {
void sendMessage(String message);
}
// Service implementations
public class EmailService implements MessagingService {
@Override
public void sendMessage(String message) {
System.out.println("Sending email: " + message);
}
}
public class SMSService implements MessagingService {
@Override
public void sendMessage(String message) {
System.out.println("Sending SMS: " + message);
}
}
// Service locator
public class ServiceLocator {
private static Map<String, Object> serviceCache = new HashMap<>();
public static MessagingService getMessagingService(String serviceType) {
// Check in cache
MessagingService service = (MessagingService) serviceCache.get(serviceType);
if (service != null) {
return service;
}
// If not in cache, create new instance
if ("EMAIL".equals(serviceType)) {
service = new EmailService();
} else if ("SMS".equals(serviceType)) {
service = new SMSService();
} else {
throw new IllegalArgumentException("Unknown service type: " + serviceType);
}
// Add to cache
serviceCache.put(serviceType, service);
return service;
}
}
// Client code
MessagingService emailService = ServiceLocator.getMessagingService("EMAIL");
emailService.sendMessage("Hello, this is an email message");
MessagingService smsService = ServiceLocator.getMessagingService("SMS");
smsService.sendMessage("Hello, this is an SMS message");
JNDI Example¶
// Service locator with JNDI
public class JNDIServiceLocator {
private InitialContext context;
private Map<String, Object> cache;
private static JNDIServiceLocator instance;
private JNDIServiceLocator() {
try {
this.context = new InitialContext();
this.cache = new HashMap<>();
} catch (NamingException e) {
throw new RuntimeException("Failed to initialize JNDI context", e);
}
}
public static synchronized JNDIServiceLocator getInstance() {
if (instance == null) {
instance = new JNDIServiceLocator();
}
return instance;
}
public Object lookup(String jndiName) {
Object service = cache.get(jndiName);
if (service != null) {
return service;
}
try {
service = context.lookup(jndiName);
cache.put(jndiName, service);
return service;
} catch (NamingException e) {
throw new ServiceLookupException("Failed to lookup service: " + jndiName, e);
}
}
}
// Usage
DataSource dataSource = (DataSource) JNDIServiceLocator.getInstance().lookup("java:comp/env/jdbc/myDB");
Connection connection = dataSource.getConnection();
When to Use the Service Locator Pattern¶
- When you need a centralized point for service lookup
- When you want to decouple service consumers from service implementations
- When you need to cache service references for performance
- When working with JNDI or other lookup mechanisms
- When you need to hide the complexity of service instantiation
Advantages¶
- Centralizes service lookup logic
- Improves performance through caching
- Decouples clients from service lookup mechanisms
- Simplifies client code
- Can be extended to support different types of services
Disadvantages¶
- Can hide dependencies (service locator anti-pattern)
- May make testing more difficult
- Violates Inversion of Control (IoC) principles
- Makes it harder to track which services are actually used
- Dependency Injection is often a better alternative in modern applications
Data Access Object Pattern¶
The Data Access Object (DAO) pattern isolates the application/business layer from the persistence layer, providing an abstract interface to the database or other persistence mechanism.
Intent¶
- Abstract and encapsulate data access mechanisms
- Provide a uniform data access API regardless of the persistence mechanism
- Hide the complexity of data access operations
- Facilitate the use of different data sources
Implementation¶
// Data model
public class User {
private Long id;
private String username;
private String email;
// Constructors
public User() {}
public User(Long id, String username, String email) {
this.id = id;
this.username = username;
this.email = email;
}
// Getters and setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
// DAO interface
public interface UserDao {
User findById(Long id);
List<User> findAll();
void save(User user);
void update(User user);
void delete(Long id);
}
// JDBC implementation
public class UserDaoJdbcImpl implements UserDao {
private Connection connection;
public UserDaoJdbcImpl() {
// Initialize connection
try {
this.connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "username", "password");
} catch (SQLException e) {
throw new RuntimeException("Failed to connect to database", e);
}
}
@Override
public User findById(Long id) {
try {
PreparedStatement stmt = connection.prepareStatement("SELECT * FROM users WHERE id = ?");
stmt.setLong(1, id);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
User user = new User();
user.setId(rs.getLong("id"));
user.setUsername(rs.getString("username"));
user.setEmail(rs.getString("email"));
return user;
}
return null;
} catch (SQLException e) {
throw new DataAccessException("Failed to find user by ID", e);
}
}
@Override
public List<User> findAll() {
List<User> users = new ArrayList<>();
try {
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
while (rs.next()) {
User user = new User();
user.setId(rs.getLong("id"));
user.setUsername(rs.getString("username"));
user.setEmail(rs.getString("email"));
users.add(user);
}
return users;
} catch (SQLException e) {
throw new DataAccessException("Failed to find all users", e);
}
}
@Override
public void save(User user) {
try {
PreparedStatement stmt = connection.prepareStatement("INSERT INTO users (username, email) VALUES (?, ?)");
stmt.setString(1, user.getUsername());
stmt.setString(2, user.getEmail());
stmt.executeUpdate();
} catch (SQLException e) {
throw new DataAccessException("Failed to save user", e);
}
}
@Override
public void update(User user) {
try {
PreparedStatement stmt = connection.prepareStatement("UPDATE users SET username = ?, email = ? WHERE id = ?");
stmt.setString(1, user.getUsername());
stmt.setString(2, user.getEmail());
stmt.setLong(3, user.getId());
stmt.executeUpdate();
} catch (SQLException e) {
throw new DataAccessException("Failed to update user", e);
}
}
@Override
public void delete(Long id) {
try {
PreparedStatement stmt = connection.prepareStatement("DELETE FROM users WHERE id = ?");
stmt.setLong(1, id);
stmt.executeUpdate();
} catch (SQLException e) {
throw new DataAccessException("Failed to delete user", e);
}
}
}
// Custom exception
public class DataAccessException extends RuntimeException {
public DataAccessException(String message, Throwable cause) {
super(message, cause);
}
}
// Client code
UserDao userDao = new UserDaoJdbcImpl();
// Create a new user
User newUser = new User();
newUser.setUsername("john_doe");
newUser.setEmail("john@example.com");
userDao.save(newUser);
// Find a user
User user = userDao.findById(1L);
// Update a user
user.setEmail("new_email@example.com");
userDao.update(user);
// Find all users
List<User> allUsers = userDao.findAll();
// Delete a user
userDao.delete(1L);
Spring Data JPA Example¶
// Entity
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String email;
// Getters and setters
}
// Repository interface
public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByUsername(String username);
List<User> findByEmail(String email);
@Query("SELECT u FROM User u WHERE u.username LIKE %:keyword% OR u.email LIKE %:keyword%")
List<User> search(@Param("keyword") String keyword);
}
// Service layer
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User findById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("User not found with id: " + id));
}
public List<User> findAll() {
return userRepository.findAll();
}
public User save(User user) {
return userRepository.save(user);
}
public void delete(Long id) {
userRepository.deleteById(id);
}
}
// Client code (Controller)
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseEntity.ok(user);
}
@GetMapping
public List<User> getAllUsers() {
return userService.findAll();
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
User savedUser = userService.save(user);
return ResponseEntity.status(HttpStatus.CREATED).body(savedUser);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build();
}
}
When to Use the DAO Pattern¶
- When you need to abstract the data access layer from the business logic
- When you want to switch between different data sources with minimal impact
- When you need to implement complex persistence logic
- When working with relational databases, NoSQL databases, or other persistence mechanisms
- When you want to centralize data access code
Advantages¶
- Separates data access logic from business logic
- Provides a clean interface to data access operations
- Makes it easier to switch between different data sources
- Improves testability of both business and data access logic
- Centralizes data access code for better maintenance
Disadvantages¶
- Can add complexity in simple applications
- May lead to many similar DAO implementations
- Can be redundant when using ORMs like Hibernate or JPA
- Needs careful design to avoid becoming a mere pass-through layer
- May introduce performance overhead if not implemented carefully
Front Controller Pattern¶
The Front Controller pattern provides a centralized entry point for handling requests in a web application.
Intent¶
- Provide a centralized request handling mechanism
- Manage request preprocessing, dispatching, and postprocessing
- Enforce consistent handling of all requests
- Centralize cross-cutting concerns like security and logging
Implementation¶
// Front controller servlet
@WebServlet("/*")
public class FrontControllerServlet extends HttpServlet {
private RequestDispatcher dispatcher;
public void init() {
dispatcher = new RequestDispatcher();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
process(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
process(request, response);
}
private void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// Preprocess the request
String path = request.getRequestURI().substring(request.getContextPath().length());
// Log request
System.out.println("Request received: " + path);
// Authenticate user (example)
if (!authenticate(request)) {
response.sendRedirect(request.getContextPath() + "/login");
return;
}
// Dispatch the request
dispatcher.dispatch(request, response, path);
// Postprocess the response
System.out.println("Request processed: " + path);
}
private boolean authenticate(HttpServletRequest request) {
// Authentication logic
HttpSession session = request.getSession(false);
return session != null && session.getAttribute("user") != null;
}
}
// Request dispatcher
public class RequestDispatcher {
private Map<String, Controller> controllerMap;
public RequestDispatcher() {
controllerMap = new HashMap<>();
// Register controllers
controllerMap.put("/users", new UserController());
controllerMap.put("/products", new ProductController());
controllerMap.put("/orders", new OrderController());
// Default controller
controllerMap.put("/", new HomeController());
}
public void dispatch(HttpServletRequest request, HttpServletResponse response, String path) throws ServletException, IOException {
// Find the appropriate controller
Controller controller = null;
for (Map.Entry<String, Controller> entry : controllerMap.entrySet()) {
if (path.startsWith(entry.getKey())) {
controller = entry.getValue();
break;
}
}
if (controller == null) {
// Use default controller or show 404
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
// Execute the controller
String view = controller.execute(request, response);
// Forward to the view
if (view != null) {
request.getRequestDispatcher("/WEB-INF/views/" + view + ".jsp").forward(request, response);
}
}
}
// Controller interface
public interface Controller {
String execute(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
// Sample controller implementations
public class HomeController implements Controller {
@Override
public String execute(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
return "home";
}
}
public class UserController implements Controller {
@Override
public String execute(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String action = request.getParameter("action");
if ("list".equals(action)) {
// Get user list
request.setAttribute("users", getUserList());
return "user-list";
} else if ("view".equals(action)) {
// Get user details
String id = request.getParameter("id");
request.setAttribute("user", getUserById(id));
return "user-details";
}
return "user-list";
}
private List<String> getUserList() {
// Sample data
return Arrays.asList("User 1", "User 2", "User 3");
}
private String getUserById(String id) {
return "User " + id;
}
}
Spring MVC Implementation¶
// Spring MVC Front Controller is DispatcherServlet (configured in web.xml or via Java config)
// Controller
@Controller
public class UserController {
@GetMapping("/users")
public String listUsers(Model model) {
model.addAttribute("users", getUserList());
return "user-list";
}
@GetMapping("/users/{id}")
public String viewUser(@PathVariable String id, Model model) {
model.addAttribute("user", getUserById(id));
return "user-details";
}
private List<String> getUserList() {
// Sample data
return Arrays.asList("User 1", "User 2", "User 3");
}
private String getUserById(String id) {
return "User " + id;
}
}
// Configuration
@Configuration
@EnableWebMvc
@ComponentScan("com.example.controllers")
public class WebConfig implements WebMvcConfigurer {
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}
}
When to Use the Front Controller Pattern¶
- When building web applications that need centralized request handling
- When you need consistent preprocessing of requests (e.g., security, logging)
- When you want to avoid code duplication across multiple servlets
- When implementing MVC architecture in a web application
- When using frameworks like Spring MVC that implement this pattern
Advantages¶
- Centralizes control logic for request handling
- Provides a single point for cross-cutting concerns
- Enforces consistent request handling
- Simplifies security implementation
- Facilitates clear separation of concerns in web applications
Disadvantages¶
- May become a bottleneck if not designed properly
- Can add complexity for simple applications
- Can become bloated if too many responsibilities are added
- Requires careful design to maintain scalability
- May introduce a learning curve for developers
Best Practices¶
-
Understand the Problem Domain: Choose patterns that match your specific requirements and context.
-
Avoid Overengineering: Don't use patterns just for the sake of using them. Apply them only when they add value.
-
Combine Patterns Effectively: Many J2EE applications use multiple patterns together to solve complex problems.
-
Consider Modern Alternatives:
- Dependency Injection (DI) can replace Service Locator
- ORM frameworks can simplify the DAO pattern
-
RESTful services can replace some traditional J2EE patterns
-
Documentation: Document why and how patterns are implemented in your application.
-
Testing: Create comprehensive tests for components implementing patterns.
-
Keep It Simple: Choose the simplest pattern that solves the problem.
-
Consistent Implementation: Apply patterns consistently throughout the application.
-
Consider Performance: Some patterns add overhead that may affect performance.
-
Maintainability First: Prioritize code that is maintainable over clever implementations.
Common Pitfalls¶
-
Pattern Overuse: Applying patterns where they're not needed increases complexity without benefits.
-
Ignoring Modern Alternatives: Some J2EE patterns have been superseded by newer approaches.
-
Rigid Implementation: Implementing patterns too rigidly without adapting to specific project needs.
-
Excessive Layering: Creating too many layers can lead to "analysis paralysis" and over-complicated code.
-
Premature Optimization: Implementing complex patterns for performance before it's proven necessary.
-
Poor Documentation: Not documenting why patterns were chosen and how they're implemented.
-
Sacrificing Simplicity: Making code complex by using patterns when simpler solutions would work.
-
Ignoring Framework Support: Modern frameworks often provide built-in support for common patterns.
-
Not Understanding Pattern Consequences: Each pattern has trade-offs that must be understood.
-
Outdated Patterns: Using patterns that are no longer considered best practice.
Modern Alternatives¶
- Dependency Injection (DI) instead of Service Locator:
- Spring and CDI provide sophisticated DI capabilities
- More testable and maintainable code
-
Makes dependencies explicit
-
Spring Data JPA instead of custom DAOs:
- Eliminates boilerplate DAO code
- Provides repositories with powerful query methods
-
Offers transaction management out of the box
-
Microservices instead of monolithic architectures:
- More scalable and maintainable
- Allows for independent deployment
-
Better fault isolation
-
API Gateways instead of Front Controller:
- More suitable for microservices architecture
- Provides additional capabilities like rate limiting and analytics
-
Examples: Spring Cloud Gateway, Netflix Zuul
-
RESTful services instead of Session Facade:
- More scalable and stateless
- Better for distributed systems
-
Easier to consume by various clients
-
GraphQL instead of Transfer Objects:
- Clients can request exactly what they need
- Reduces over-fetching and under-fetching
-
More flexible than fixed DTOs
-
Reactive programming instead of traditional request/response:
- Better handling of asynchronous operations
- More efficient resource utilization
-
Examples: Spring WebFlux, RxJava
-
Container orchestration instead of custom clustering:
- Kubernetes provides advanced deployment, scaling, and management
- More robust and feature-rich
-
Industry standard approach
-
Configuration services instead of property files:
- Centralized configuration management
- Dynamic configuration updates
-
Examples: Spring Cloud Config Server, Consul
-
Cloud-native patterns:
- Circuit Breaker, Bulkhead, Sidecar
- More resilient distributed systems
- Examples: Resilience4j, Istio