Category: Programming

  • ✅ Recommended Strategy

    ✅ Recommended Strategy

    Use a Hierarchy of Custom Exceptions with Specific Messages

    1. Define a base exception class

    Create a generic custom exception that extends RuntimeException (or Exception if you need checked exceptions).

    public class ApplicationException extends RuntimeException {
        private final String errorCode;
    
        public ApplicationException(String message, String errorCode) {
            super(message);
            this.errorCode = errorCode;
        }
    
        public String getErrorCode() {
            return errorCode;
        }
    }
    

    2. Create a few meaningful subclasses for common domains

    Use subclasses only for distinct categories, not every input field.

    public class ValidationException extends ApplicationException {
        public ValidationException(String message) {
            super(message, "VALIDATION_ERROR");
        }
    }
    
    public class ResourceNotFoundException extends ApplicationException {
        public ResourceNotFoundException(String message) {
            super(message, "RESOURCE_NOT_FOUND");
        }
    }
    

    3. Throw exceptions with detailed messages

    Use informative messages, but reuse exception classes.

    if (email == null || !email.contains("@")) {
        throw new ValidationException("Invalid email address: " + email);
    }
    
    if (user == null) {
        throw new ResourceNotFoundException("User with ID " + userId + " not found.");
    }
    

    4. Centralize exception handling

    Use @ControllerAdvice in Spring Boot to map exceptions to HTTP responses.

    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(ValidationException.class)
        public ResponseEntity<ApiError> handleValidationException(ValidationException ex) {
            return ResponseEntity.badRequest().body(new ApiError(ex.getErrorCode(), ex.getMessage()));
        }
    
        @ExceptionHandler(ResourceNotFoundException.class)
        public ResponseEntity<ApiError> handleNotFoundException(ResourceNotFoundException ex) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND)
                                 .body(new ApiError(ex.getErrorCode(), ex.getMessage()));
        }
    }
    

    🔄 Trade-offs

    StrategyProsCons
    One Exception per InputMax granularityToo many classes, high maintenance
    Generic Exception OnlySimpleHard to trace errors, poor UX/API feedback
    Domain-Based Custom Exceptions (✅)Balanced, readable, scalableSlightly more boilerplate

    📌 Summary

    • Don’t create one exception class per field or input.
    • Don’t rely solely on generic exceptions with vague messages.
    • Do create a few well-named custom exceptions per domain concern (validation, business rule, resource not found).
    • Do include specific messages and codes for clarity and front-end/API consumption.

    If you’re building APIs, error codes + structured JSON responses improve DX (developer experience) and UX.

    Let me know if you want a boilerplate for API exception handling in Spring Boot.


    Other considerations

    There are several important considerations when designing and handling exceptions in Java applications, especially for APIs or enterprise-level systems. Here’s a breakdown of strategic, practical, and technical points beyond the basic structure:


    🔍 1. Checked vs Unchecked Exceptions

    • Checked Exceptions (extends Exception): Force callers to handle or declare them.
      • Good for recoverable errors (e.g. file not found, invalid config).
      • Can pollute method signatures and reduce readability.
    • Unchecked Exceptions (extends RuntimeException): Don’t require handling.
      • Better for programming errors, validation failures, or business rules.
      • Preferred in modern Java and frameworks like Spring.

    👉 Recommendation: Use unchecked exceptions for business logic and REST APIs unless you’re dealing with truly recoverable scenarios (e.g., IO or external service failures).


    🧩 2. Message Structure: Human + Machine Friendly

    • Provide:
      • A clear human-readable message
      • An error code (short, unique identifier, e.g. USER_NOT_FOUND)
      • Optionally: a field name, if applicable
    {
      "error": {
        "code": "EMAIL_INVALID",
        "message": "The email format is invalid",
        "field": "email"
      }
    }
    

    This format helps:

    • Frontend display and localization
    • Developers debugging issues
    • Automated systems parsing errors

    🗂 3. Use of ErrorCode Enum or Registry

    Instead of string literals scattered throughout your code, centralize error codes:

    public enum ErrorCode {
        INVALID_EMAIL("EMAIL_INVALID"),
        USER_NOT_FOUND("USER_NOT_FOUND");
    
        private final String code;
    
        ErrorCode(String code) {
            this.code = code;
        }
    
        public String getCode() {
            return code;
        }
    }
    

    This:

    • Prevents duplication
    • Enables translation
    • Supports analytics/logs

    📦 4. Payload-rich Exceptions

    Sometimes, it’s useful for exceptions to carry more context, like field names, rejected values, etc.

    public class FieldValidationException extends ValidationException {
        private final String field;
        private final Object rejectedValue;
    
        public FieldValidationException(String field, Object rejectedValue, String message) {
            super(message);
            this.field = field;
            this.rejectedValue = rejectedValue;
        }
    
        // getters
    }
    

    You can serialize this in your @ControllerAdvice.


    🧪 5. Testability and Observability

    • Design exceptions so that unit and integration tests can assert them cleanly.
    • Log exceptions with structured messages or MDC context (e.g. requestId).
    • Use tools like:
      • Sentry, Datadog, or Elastic Stack for centralized logging
      • AOP or HandlerInterceptor to inject context (user ID, trace ID)

    💡 6. Avoid Swallowing Exceptions

    Don’t silently ignore exceptions unless you’re 100% sure it’s safe:

    try {
        doSomething();
    } catch (Exception e) {
        // ❌ Don't just do nothing or log without action
        logger.error("Something went wrong", e); // ✅
        throw new ApplicationException("Internal processing error", "INTERNAL_ERROR");
    }
    

    🔒 7. Security and Internal Data

    Be cautious not to expose internal details like stack traces, SQL errors, or implementation-specific info in API responses.

    ✅ Do this:

    {
      "code": "INTERNAL_ERROR",
      "message": "Something went wrong. Please contact support."
    }
    

    ❌ Not this:

    {
      "message": "java.sql.SQLIntegrityConstraintViolationException: Duplicate entry 'foo@example.com'..."
    }
    

    🧘 Final Thoughts

    Designing a good exception strategy is about clarity, maintainability, and robustness. A few key principles:

    • Be consistent in your error structure.
    • Surface enough context for clients to recover or correct input.
    • Don’t overdo exception classes, but don’t go fully generic either.
    • Integrate with observability tools to monitor exception patterns.
  • Singleton Design Pattern or Configuration Beans?

    Singleton Design Pattern or Configuration Beans?

    Choosing the right approach in Java architecture

    When designing a Java application—particularly in the context of modern enterprise systems—developers often face a fundamental question:
    Should I use the Singleton design pattern or rely on configuration beans managed by a framework like Spring?

    While both approaches can achieve a shared goal—ensuring a single instance of a class—they differ drastically in terms of flexibility, testability, and architectural soundness.

    In this article, we’ll explore the pros and cons of each, and explain why configuration beans are generally the better architectural choice in Spring-based applications.


    ☕ The Classic Singleton Pattern

    The Singleton pattern is one of the most well-known creational design patterns in object-oriented programming. It ensures that a class has only one instance and provides a global point of access to it.

    ✅ Typical Use Case

    public class SingletonService {
        private static SingletonService instance;
    
        private SingletonService() {}
    
        public static SingletonService getInstance() {
            if (instance == null) {
                instance = new SingletonService();
            }
            return instance;
        }
    }
    

    This approach works fine in plain Java applications, but it comes with architectural limitations:

    ❌ Drawbacks

    • Manual lifecycle management
    • Difficult to inject dependencies
    • Not easily mockable in tests
    • Requires careful thread-safety handling
    • Global access encourages tight coupling

    🌱 The Spring Way: Configuration Beans

    Spring takes care of object lifecycle via Inversion of Control (IoC). Beans defined in configuration classes or annotated with @Component are singletons by default, unless otherwise specified.

    ✅ Example Using @Configuration

    @Configuration
    public class AppConfig {
        
        @Bean
        public MyService myService() {
            return new MyService();
        }
    }
    

    Spring manages this bean as a singleton, handles dependency injection, and ensures thread safety automatically.


    🔍 Comparing the Two Approaches

    FeatureSingleton ClassSpring Configuration Bean
    Instance ControlManualAutomatic (IoC container)
    Thread SafetyNeeds custom handlingHandled by Spring
    Dependency InjectionManual/staticFully supported
    TestabilityHard to mock or overrideEasily mockable
    Scope ManagementSingleton onlySingleton, prototype, etc.
    ExtensibilityLimitedHighly flexible
    Environment SupportHardcodedSpring profiles support

    🧪 Why Configuration Beans Are Architecturally Superior

    1. Clean Dependency Management
      • Spring promotes constructor injection, reducing tight coupling and making your code more maintainable.
    2. Built-in Singleton Support
      • Beans in Spring are singletons by default, with no need to write boilerplate getInstance() code.
    3. Testability
      • With tools like @MockBean or @TestConfiguration, beans can be easily replaced or mocked in unit and integration tests.
    4. Flexible Configuration
      • You can manage different beans for different environments using @Profile, or override beans for test vs. production.
    5. Better Lifecycle Management
      • Spring handles initialization, destruction, and scope without developer intervention.

    🧭 When to Use Each

    SituationRecommended Approach
    Spring application✅ Configuration Bean
    Utility class with static methods✅ Static Utility Class
    Non-Spring app needing one instance☑️ Singleton (carefully)
    Complex object graph or DI needed✅ Configuration Bean
    Multi-environment configuration✅ Configuration Bean with @Profile

    📝 Conclusion

    While the Singleton pattern has its place—particularly in simpler, non-framework applications—it’s often a liability in modern Java architecture due to its inflexibility and testing challenges.

    In contrast, Spring configuration beans embrace the principles of Dependency Injection, testability, and scalability, making them a more robust and future-proof solution.

    Bottom line: If you’re using Spring (or any IoC container), prefer configuration beans over Singleton classes. Let the framework manage lifecycle and dependencies so you can focus on writing clean, modular code.

  • 🚨 Already Had Problems with Concurrency?

    🚨 Already Had Problems with Concurrency?

    Here’s How to Face and Overcome It in Spring Boot

    Concurrency issues in applications—especially those backed by relational databases—can cause data inconsistencies, lost updates, or phantom reads that are hard to detect and even harder to debug. Have you ever seen something like:

    • A user balance suddenly incorrect?
    • A stock quantity going negative even with validation?
    • Two administrators editing the same record, with one overwriting the other’s changes?

    If yes, you’re not alone. These are classic symptoms of concurrent modification problems. The good news is: JPA gives you tools to overcome them.

    In this article, we’ll explore two powerful concurrency control strategies:

    1. Pessimistic Locking (via @Lock)
    2. Optimistic Locking (via @Version)

    We’ll dive into:

    • 🔍 When to use each approach
    • 🧪 Practical code examples
    • 🧭 How to choose between them

    🔒 Pessimistic Locking: The “Reserve Now, Edit Later” Approach

    Pessimistic locking assumes conflict is likely. When a transaction reads a record, it immediately locks it, preventing others from making any changes until it’s done.

    🧠 When to Use It

    • You expect high contention on records (e.g., financial systems).
    • Losing an update is unacceptable.
    • Transactions involve multiple steps, and integrity must be guaranteed.

    🧱 Example: Booking a Seat on a Flight

    @Entity
    public class Seat {
        @Id
        private Long id;
    
        private boolean booked;
    
        private String passengerName;
    }
    

    Repository with Pessimistic Lock:

    public interface SeatRepository extends JpaRepository<Seat, Long> {
        @Lock(LockModeType.PESSIMISTIC_WRITE)
        @Query("SELECT s FROM Seat s WHERE s.id = :id")
        Optional<Seat> findSeatForUpdate(@Param("id") Long id);
    }
    

    Service Example:

    @Transactional
    public void bookSeat(Long seatId, String passenger) {
        Seat seat = seatRepository.findSeatForUpdate(seatId)
                .orElseThrow(() -> new RuntimeException("Seat not found"));
    
        if (seat.isBooked()) {
            throw new IllegalStateException("Seat already booked");
        }
    
        seat.setBooked(true);
        seat.setPassengerName(passenger);
        seatRepository.save(seat);
    }
    

    🧩 How It Works: The database places a row-level lock. Any other transaction trying to update the same row must wait or fail (depending on your DB settings).

    Pros:

    • Guarantees consistency.
    • Easy to understand.

    Cons:

    • Lower performance under high load.
    • Risk of deadlocks if not managed carefully.

    🔄 Optimistic Locking: The “Try First, Fail Fast” Approach

    Optimistic locking assumes that conflict is rare. Instead of locking the row, it uses a @Version field to detect changes before committing.

    🧠 When to Use It

    • Low probability of conflict.
    • You want high performance and scalability.
    • Conflict resolution is acceptable via retries or user feedback.

    🧱 Example: Editing a Product in an Admin Panel

    @Entity
    public class Product {
        @Id
        private Long id;
    
        private String name;
    
        private BigDecimal price;
    
        @Version
        private Integer version;
    }
    

    Service Example:

    @Transactional
    public void updateProduct(Product updatedProduct) {
        Product existing = productRepository.findById(updatedProduct.getId())
            .orElseThrow(() -> new RuntimeException("Product not found"));
    
        existing.setName(updatedProduct.getName());
        existing.setPrice(updatedProduct.getPrice());
        // version is automatically checked when save is called
    }
    

    Behavior:

    If two users fetch the same product and both try to update it:

    • The first one succeeds.
    • The second fails with an OptimisticLockException, because the version no longer matches.

    Pros:

    • No locks; better throughput.
    • Great for read-heavy systems.

    Cons:

    • You must handle retries.
    • Users might lose work unless you give UI feedback.

    🧭 Choosing Between Pessimistic vs Optimistic Locking

    Feature/ScenarioPessimistic LockingOptimistic Locking
    Conflict likelihoodHighLow
    Critical transactions (e.g., banking)✅ Best choice❌ Risky
    Performance / scalability❌ Blocking, lower throughput✅ Non-blocking, faster
    Multi-step operations (e.g., check + update)✅ Needed❌ Race condition risk
    Distributed or web applications (REST APIs)❌ Can lead to timeouts✅ More robust
    Need for user feedback on conflict❌ Hard to provide✅ Easy to detect & inform

    🛠 Final Tip: Always Combine With @Transactional

    Both approaches require transactions to be effective. Never use locking logic without @Transactional, or you’ll break consistency guarantees.


    ✅ Conclusion

    Concurrency problems don’t have to be mysterious or unmanageable. With JPA’s pessimistic and optimistic locking tools, Spring Boot gives you robust options to:

    • Prevent data corruption
    • Handle conflicts gracefully
    • Build resilient systems

    🧭 Choose pessimistic locking when data integrity is non-negotiable, and choose optimistic locking when performance and user experience matter most.

    📚 Know More: Other Strategies to Handle Concurrency

    While optimistic and pessimistic locking are essential tools for managing concurrency in database operations, they’re not the only options. Depending on your system architecture and the type of data you’re protecting, you might benefit from additional or alternative concurrency control techniques.

    Here are several other common strategies:


    🔄 Software-Level Concurrency Controls

    1. Synchronized Code Blocks (Java)
      • Use synchronized or ReentrantLock for in-memory locking.
      • Best for simple JVM-based state sharing in single-node applications.
    2. Application-Level Queuing
      • Queue requests (e.g., with a message broker) to serialize access to resources.
      • Reduces write contention by delegating processing to a single-threaded consumer.
    3. Atomic Operations
      • Use atomic types like AtomicInteger, AtomicReference, or ConcurrentHashMap for low-level concurrent data manipulation.
    4. Java Concurrency Utilities
      • Use Semaphore, CountDownLatch, ExecutorService, etc., for precise thread-level control.

    🌐 Distributed Concurrency Controls

    1. Distributed Locks
      • Leverage Redis (e.g., Redisson), Zookeeper, or a database table to coordinate access across application nodes.
      • Suitable for microservices or clustered environments.
    2. ShedLock for Scheduled Tasks
      • Prevents multiple instances of the same Spring Boot app from executing a scheduled task concurrently.
      • Commonly used in distributed cron jobs.

    📊 Comparison Table: Concurrency Strategies at a Glance

    StrategyScopeBlocking?Distributed?Use Case Example
    Optimistic LockingDatabase rowNoYesEditing product details in a CMS
    Pessimistic LockingDatabase rowYesYesBooking a flight seat
    Synchronized / ReentrantLockJVM memoryYesNoAccessing shared cache in a single-node app
    Atomic VariablesJVM memoryNoNoUpdating counters, flags
    Distributed Lock (Redis/ZK)Global clusterYesYesUpdating shared resources across nodes
    Application Queue (RabbitMQ, Kafka)Service-levelN/AYesProcessing bank transactions sequentially
    ShedLockScheduled tasksYesYesCron jobs in a multi-node Spring Boot app

    🧭 Final Thoughts

    Understanding when and how to use each strategy is key to building reliable systems:

    • For data consistency, start with JPA locking mechanisms.
    • For application-level control, use Java concurrency tools or queues.
    • For clustered environments, distributed locking or queuing is essential.

    Combine these techniques thoughtfully based on your system’s architecture, performance goals, and failure tolerance.

  • “Which Java Collection Should I Use?”

    “Which Java Collection Should I Use?”

    Choosing the Right Data Structure for Real-World Problems

    Have you ever asked yourself which data structure you should use to store your collection of items?
    It’s a common question every Java developer faces, especially when writing scalable and maintainable code.

    Java’s Collections Framework offers a variety of implementations for lists, sets, and maps—but using the right one in the right context can make a big difference in performance, readability, and correctness.

    This post explores six commonly used collection types:
    👉 ArrayList, LinkedList, HashSet, TreeSet, HashMap, and TreeMap
    And it gives real-world examples where each shines, so you can think critically about which one fits your situation best.


    ArrayList – When Fast Access Matters

    Real-World Problem: You’re building a search suggestion dropdown that displays a list of recently typed queries.

    • Why it fits:
      ArrayList gives constant-time access by index. You can instantly show the N-th recent search. Since you’re only adding at the end or reading in order, it’s perfect.
    • Avoid if: You’re doing lots of insertions or deletions in the middle of the list.

    🔁 LinkedList – When Frequent Insertions or Deletions Are Needed

    Real-World Problem: You’re building a playlist feature where users can frequently rearrange songs (move up/down), insert between, or remove them.

    • Why it fits:
      LinkedList is a doubly-linked list. It handles frequent adds/removals from anywhere in the list faster than an ArrayList, especially for large lists.
    • Avoid if: You need random access to elements; get(index) is slow.

    🧺 HashSet – When Uniqueness Is the Only Requirement

    Real-World Problem: You’re storing a list of all email addresses that signed up for a beta program, and duplicates must be prevented.

    • Why it fits:
      HashSet automatically ensures uniqueness and gives constant-time add, remove, and contains operations. Perfect for preventing duplicate entries.
    • Avoid if: You care about insertion order or need sorted output.

    🌲 TreeSet – When You Need Sorted, Unique Data

    Real-World Problem: You’re storing a leaderboard with player scores and want to keep them in ascending or descending order, without duplicates.

    • Why it fits:
      TreeSet keeps elements sorted according to their natural order (or a custom comparator) and ensures uniqueness.
    • Avoid if: You don’t need sorting—HashSet is faster.

    🗂️ HashMap – When You Need Key-Based Fast Lookup

    Real-World Problem: You’re building a dictionary app where users type a word and you fetch its definition instantly.

    • Why it fits:
      HashMap offers constant-time retrieval for keys. It’s ideal for any key-value lookup scenario, like config values, routing tables, or caching.
    • Avoid if: You need sorted keys or order-sensitive operations.

    🧭 TreeMap – When You Need Sorted Keys

    Real-World Problem: You’re storing time-series sensor data where each data point has a timestamp as the key, and you want to retrieve them in chronological order.

    • Why it fits:
      TreeMap keeps keys sorted, making it perfect for time-based logs, ranges, or navigation systems.
    • Avoid if: Key order doesn’t matter—HashMap is more performant.

    📚 Code Examples + Performance Overview

    We’ll now walk through each collection with:

    1. 🔧 A practical Java code example
    2. ⚙️ Performance notes for common operations

    ArrayList – Fast Access, Ordered Collection

    List<String> recentSearches = new ArrayList<>();
    recentSearches.add("Java Streams");
    recentSearches.add("Spring Boot");
    recentSearches.add("Docker");
    
    // Fast access by index
    System.out.println(recentSearches.get(1)); // Output: Spring Boot
    

    ⚙️ Performance

    OperationComplexity
    add() (end)O(1)
    get(index)O(1)
    add(index)O(n)
    remove(index)O(n)

    ✅ Use for: access-heavy, append-only collections
    ⚠️ Avoid for: frequent middle inserts/removals


    🔁 LinkedList – Efficient Inserts & Deletes

    List<String> playlist = new LinkedList<>();
    playlist.add("Song A");
    playlist.add("Song B");
    playlist.add(1, "Song C"); // Insert in the middle
    
    playlist.remove("Song A");
    System.out.println(playlist); // [Song C, Song B]
    

    ⚙️ Performance

    OperationComplexity
    add()/remove (ends)O(1)
    add/remove (middle)O(n)
    get(index)O(n)

    ✅ Use for: queues, stacks, dynamic lists
    ⚠️ Avoid for: random access needs


    🧺 HashSet – Unique, Unordered Items

    Set<String> emails = new HashSet<>();
    emails.add("john@example.com");
    emails.add("john@example.com"); // Duplicate ignored
    emails.add("jane@example.com");
    
    System.out.println(emails); // [john@example.com, jane@example.com] (order not guaranteed)
    

    ⚙️ Performance

    OperationComplexity
    add()O(1)
    remove()O(1)
    contains()O(1)

    ✅ Use for: deduplication, membership checks
    ⚠️ Avoid for: ordered or sorted results


    🌲 TreeSet – Unique & Sorted

    Set<Integer> scores = new TreeSet<>();
    scores.add(300);
    scores.add(100);
    scores.add(200);
    
    System.out.println(scores); // [100, 200, 300]
    

    ⚙️ Performance

    OperationComplexity
    add()O(log n)
    remove()O(log n)
    contains()O(log n)

    ✅ Use for: sorted data, range queries
    ⚠️ Avoid for: high-volume add/remove where sorting isn’t needed


    🗂️ HashMap – Key-Based Lookup (Unordered)

    Map<String, String> dictionary = new HashMap<>();
    dictionary.put("Java", "A programming language");
    dictionary.put("Spring", "A Java framework");
    
    System.out.println(dictionary.get("Java")); // A programming language
    

    ⚙️ Performance

    OperationComplexity
    put()O(1)
    get(key)O(1)
    remove()O(1)

    ✅ Use for: fast lookups, caching, config storage
    ⚠️ Avoid for: sorted or ordered key access


    🧭 TreeMap – Sorted Keys, Navigable

    Map<Integer, String> logs = new TreeMap<>();
    logs.put(1700, "Start process");
    logs.put(1730, "Mid-point reached");
    logs.put(1800, "Process complete");
    
    System.out.println(logs); // Keys in sorted order: {1700=..., 1730=..., 1800=...}
    

    ⚙️ Performance

    OperationComplexity
    put()O(log n)
    get()O(log n)
    remove()O(log n)

    ✅ Use for: sorted maps, time-series, navigation by key
    ⚠️ Avoid for: massive inserts where sorting isn’t needed


    ArrayList or TreeMap?

    Use ArrayList + Collections.sort() if:

    • You add many elements quickly, then sort once before accessing them.
    • You don’t need automatic sorting after each insert.
    • You can tolerate O(n log n) cost only when needed.
    List<Integer> list = new ArrayList<>();
    list.add(5);
    list.add(1);
    list.add(3);
    
    Collections.sort(list); // O(n log n) when you want sorting
    

    Pros:

    • Faster inserts (O(1) per element).
    • More efficient if sorting is infrequent.

    Cons:

    • Data is not always sorted.
    • Sorting must be manually triggered.

    Use TreeMap (or TreeSet) if:

    • You need elements always kept in sorted order.
    • You frequently add, remove, and retrieve items in sorted order.
    • Sorted access (e.g., range queries or iteration in order) is frequent.
    Map<Integer, String> sortedMap = new TreeMap<>();
    sortedMap.put(5, "five");
    sortedMap.put(1, "one");
    sortedMap.put(3, "three");
    // Always sorted by keys
    

    Pros:

    • Automatically sorted.
    • Great for range-based access, floor/ceiling queries.

    Cons:

    • Slower insertions (O(log n)).
    • Slightly higher memory overhead.

    🔍 Summary Table

    FeatureArrayList + sort()TreeMap
    Insert performanceO(1)O(log n)
    Maintain sorted order❌ (manual sort)✅ (always sorted by key)
    Sorted iterationAfter sort onlyAlways sorted
    Use caseBatch sort, occasional orderContinuous ordered access

    💡 Choose TreeMap if sorting is a core requirement throughout the lifecycle.
    Choose ArrayList + sort() if you’re optimizing for bulk inserts followed by occasional sorting.

    Summary of all java collections

    CollectionTypeImplementsMain PurposeKey Characteristics
    ArrayListListRandomAccess, ListDynamic array for fast access and iterationFast random access, slow inserts/removals in the middle, ordered, allows duplicates
    LinkedListListDeque, ListDoubly-linked list for efficient insertion/deletionSlower access time than ArrayList, fast inserts/removals, ordered, allows duplicates
    VectorListRandomAccess, ListThread-safe dynamic array (legacy)Synchronized, slower than ArrayList in single-threaded contexts
    StackListVector (extends)LIFO stack implementationThread-safe, legacy class, now replaced by Deque
    HashSetSetSetStore unique elements with no orderBacked by HashMap, no duplicates, fast lookup
    LinkedHashSetSetSetStore unique elements maintaining insertion orderPreserves insertion order, slightly slower than HashSet
    TreeSetSetNavigableSetStore sorted unique elementsSorted order (natural or custom), slower than HashSet, no duplicates
    PriorityQueueQueueQueueQueue elements with priority (min-heap by default)Elements ordered by priority, not thread-safe
    ArrayDequeQueueDequeResizable array for double-ended queue (stack or queue)Faster than Stack or LinkedList for stack/queue operations
    HashMapMapMapStore key-value pairs with no specific orderAllows one null key, fast lookup, not synchronized
    LinkedHashMapMapMapKey-value pairs maintaining insertion orderPredictable iteration order, slightly slower than HashMap
    TreeMapMapNavigableMapSorted map based on keysSorted by natural or custom comparator, slower than HashMap
    HashtableMapMapThread-safe key-value store (legacy)Synchronized, slower, no null keys or values
    ConcurrentHashMapMapConcurrentMapThread-safe, high-performance concurrent mapAllows concurrent reads and controlled writes, no null keys/values
    EnumSetSetSetSpecialized set for use with enum typesFast and compact, only works with enums
    EnumMapMapMapSpecialized map for enum keysEfficient and compact, only works with enum keys
    WeakHashMapMapMapMap with weak keys for memory-sensitive cachingKeys are GC’d when no longer referenced, good for caches
    IdentityHashMapMapMapMap that uses == instead of .equals() for comparing keysNot commonly used, but useful in specific identity-based contexts
  • Ever Struggled Sending Dev Emails to Prod?

    Ever Struggled Sending Dev Emails to Prod?

    Use @Profile to Stop That Madness

    Situation

    We’ve all been there: you’re building a feature that sends emails—maybe it’s a password reset, a newsletter, or a welcome message. You test it locally, everything looks good, and then… you forget to switch something before deploying. Suddenly, real users are receiving test emails with dummy links or placeholder content. Ouch.

    Even worse? Maybe you’re in a test environment, and emails aren’t sent at all, because the SMTP server is missing, misconfigured, or the credentials expired. Now you’re debugging not your app, but infrastructure.

    In environments like dev, test, and production, the behavior of email sending should be different, but the code should stay the same.

    Task

    The task is clear: prevent email logic from leaking into the wrong environment. Your dev/test environments should not send real emails, but your prod environment must send them reliably. And you don’t want fragile if (env.equals("prod")) logic scattered across your codebase.

    You want clean, maintainable, environment-specific email behavior with zero risk of mixing environments.

    Action

    Enter Spring’s @Profile.

    Using @Profile, you can define multiple implementations of the same interface and let Spring automatically choose the right one depending on the active environment. Let’s look at an example:

    Step 1: Define an Interface

    public interface MailSender {
        void send(String to, String message);
    }
    

    Step 2: Create Environment-Specific Implementations

    @Component
    @Profile("prod")
    public class SmtpMailSender implements MailSender {
        public void send(String to, String message) {
            // Send email via SMTP
            System.out.println("SMTP: Email sent to " + to);
        }
    }
    
    @Component
    @Profile("dev")
    public class MockMailSender implements MailSender {
        public void send(String to, String message) {
            // Log email instead of sending
            System.out.println("DEV: Mock email to " + to + ": " + message);
        }
    }
    

    Now Spring will only register one of them based on your active profile.

    Step 3: Inject and Use Without Worry

    @Service
    public class AccountService {
    
        private final MailSender mailSender;
    
        public AccountService(MailSender mailSender) {
            this.mailSender = mailSender;
        }
    
        public void sendWelcomeEmail(String userEmail) {
            mailSender.send(userEmail, "Welcome to our app!");
        }
    }
    

    Step 4: Activate Profiles Easily

    • In application.properties: propertiesCopiarEditarspring.profiles.active=dev
    • Or via CLI:
    java -Dspring.profiles.active=prod -jar app.jar

    Result

    Now you can:

    • Safely test email logic in dev without ever risking real emails going out.
    • See mock output in logs to verify formatting and triggers.
    • Ensure real emails go out in production with zero changes to your business logic.
    • Avoid environment conditionals and keep your code clean and maintainable.

    Final Considerations: SOLID Principles in Action

    This approach isn’t just a trick — it’s an application of solid software design principles:

    • S – Single Responsibility: Each MailSender implementation has one job—either mock or real sending.
    • O – Open/Closed Principle: You can add new environments (test, qa, staging) without touching existing code.
    • L – Liskov Substitution: Both MockMailSender and SmtpMailSender can be used interchangeably via the MailSender interface.
    • I – Interface Segregation: The interface is simple, with just the method needed to send an email.
    • D – Dependency Inversion: AccountService depends on an abstraction (MailSender), not a concrete implementation.

    By leveraging Spring’s @Profile, you’re not just solving a practical problem — you’re applying architectural discipline that will scale with your app.

  • Mastering IoC in Java

    Mastering IoC in Java

    10 Powerful Features Explained with STAR Method, SOLID Principles & Real Code

    Understanding Inversion of Control (IoC) is essential for writing maintainable, scalable, and testable Java applications. This post explores 10 IoC features using real-world situations, and shows how they align with SOLID principles — with real code to bring everything to life.


    1. Decoupled Code

    • Situation: You have a BillingService that directly creates and uses a PaymentGateway, making testing and replacement hard.
    • Task: Allow BillingService to work independently of specific gateway implementations.
    • Action: Extract PaymentGateway to an interface and inject it from outside.
    • Result: BillingService now works with any payment gateway, even mocks for testing.

    SOLID Alignment

    • S (SRP): Each class has a single responsibility.
    • D (DIP): High-level modules do not depend on low-level modules.

    Code Example

    // STEP 1: Define an abstraction (interface) for the payment behavior
    public interface PaymentGateway {
        // This method allows any implementation to process a payment
        void charge(String accountId, double amount);
    }
    
    // STEP 2: Provide a concrete implementation of the interface
    public class StripeGateway implements PaymentGateway {
    
        @Override
        public void charge(String accountId, double amount) {
            // Simulating a call to Stripe's payment processing system
            System.out.println("Charging $" + amount + " to account " + accountId + " via Stripe.");
        }
    }
    
    // STEP 3: The BillingService depends on the abstraction, not a specific implementation
    public class BillingService {
    
        // This is the dependency (PaymentGateway), but we're not creating it here
        private final PaymentGateway gateway;
    
        // Constructor Injection: we receive the dependency from outside
        public BillingService(PaymentGateway gateway) {
            this.gateway = gateway;
        }
    
        // This method can now work with ANY PaymentGateway implementation
        public void processPayment(String accountId, double amount) {
            System.out.println("BillingService: Processing payment...");
            gateway.charge(accountId, amount);
        }
    }
    
    // STEP 4: Simulate an application context that wires everything together
    public class Main {
        public static void main(String[] args) {
            // Instead of hardcoding dependencies inside BillingService,
            // we create them here — acting like a basic IoC container
    
            PaymentGateway gateway = new StripeGateway(); // could be replaced with another gateway
            BillingService billingService = new BillingService(gateway);
    
            // Now we use the service as usual
            billingService.processPayment("acct-123", 49.99);
        }
    }
    

    2. Dependency Injection

    • Situation: EmailService internally creates a SmtpClient, causing tight coupling.
    • Task: Enable the injection of a flexible mail client.
    • Action: Use constructor injection.
    • Result: The service can be used with mocks or real implementations.

    SOLID Alignment

    • O (OCP): You can add new email clients without modifying EmailService.

    Code Example

    public interface MailClient {
        void sendEmail(String to, String subject, String body);
    }
    
    public class SmtpClient implements MailClient {
        public void sendEmail(String to, String subject, String body) {
            System.out.println("Sending email via SMTP...");
        }
    }
    
    public class EmailService {
        private final MailClient mailClient;
    
        public EmailService(MailClient mailClient) {
            this.mailClient = mailClient;
        }
    
        public void notify(String userEmail) {
            mailClient.sendEmail(userEmail, "Welcome", "Thanks for registering!");
        }
    }
    

    3. Centralized Configuration

    • Situation: All classes instantiate their dependencies manually.
    • Task: Centralize object creation and wiring.
    • Action: Use a configuration class or framework like Spring.
    • Result: A single source of truth for dependencies.

    SOLID Alignment

    • S (SRP): Objects focus on behavior, not on instantiation logic.

    Code Example (Spring-style config)

    @Configuration
    public class AppConfig {
    
        @Bean
        public MailClient mailClient() {
            return new SmtpClient();
        }
    
        @Bean
        public EmailService emailService() {
            return new EmailService(mailClient());
        }
    }
    

    4. Lifecycle Management

    • Situation: Developers manually initialize and destroy resources.
    • Task: Automate resource lifecycle hooks.
    • Action: Use annotations like @PostConstruct and @PreDestroy.
    • Result: Cleaner and safer resource handling.

    SOLID Alignment

    • S (SRP): Initialization logic is separated and container-managed.

    Code Example

    @Component
    public class FileCache {
    
        @PostConstruct
        public void init() {
            System.out.println("Initializing cache...");
        }
    
        @PreDestroy
        public void shutdown() {
            System.out.println("Cleaning up cache...");
        }
    }
    

    5. Event Propagation

    • Situation: Components directly call each other to handle events.
    • Task: Decouple them using event broadcasting.
    • Action: Use an event publisher/subscriber model.
    • Result: Independent, modular event handling.

    SOLID Alignment

    • O (OCP), D (DIP): Listeners can be added without modifying publishers.

    Code Example (Spring-style)

    public class UserRegisteredEvent extends ApplicationEvent {
        public final String email;
        public UserRegisteredEvent(Object source, String email) {
            super(source);
            this.email = email;
        }
    }
    
    @Component
    public class WelcomeEmailListener {
        @EventListener
        public void onUserRegistered(UserRegisteredEvent event) {
            System.out.println("Sending welcome email to " + event.email);
        }
    }
    

    6. AOP (Aspect-Oriented Programming) Integration

    • Situation: Logging, security, or transactions clutter core logic.
    • Task: Separate cross-cutting concerns.
    • Action: Use AOP to define reusable, injectable behavior.
    • Result: Core logic stays clean and focused.

    SOLID Alignment

    • S (SRP): Separate concerns into aspects.

    Code Example

    @Aspect // Marks this class as an Aspect
    @Component // Enables Spring to manage this bean
    public class LoggingAspect {
    
        // Pointcut expression for any method in the service package
        @Before("execution(* com.example.service.*.*(..))")
        public void logBefore(JoinPoint joinPoint) {
            // Logging before method execution
            System.out.println("LOG: Calling method " + joinPoint.getSignature().getName() +
                " with arguments: " + java.util.Arrays.toString(joinPoint.getArgs()));
        }
    }

    7. Bean Scoping

    • Situation: You need a new object each time in one context, but not in another.
    • Task: Define the scope of object lifecycles.
    • Action: Use @Scope to specify scope.
    • Result: Proper reuse or creation based on context.

    SOLID Alignment

    • S (SRP): Classes don’t manage their own lifecycles.

    Code Example

    @Component
    @Scope("prototype")
    public class CommandHandler {
        public void execute() {
            System.out.println("Executing command...");
        }
    }
    

    8. Lazy Initialization

    • Situation: Heavy objects are created eagerly, even when unused.
    • Task: Load only when needed.
    • Action: Use @Lazy.
    • Result: Faster startup and lower memory usage.

    SOLID Alignment

    • I (ISP): Only needed dependencies are loaded when required.

    Code Example

    @Component
    @Lazy
    public class AnalyticsService {
        public AnalyticsService() {
            System.out.println("Expensive analytics setup...");
        }
    }
    

    9. Auto-wiring

    • Situation: Manually injecting dependencies becomes tedious and error-prone.
    • Task: Let the framework resolve dependencies automatically.
    • Action: Use @Autowired.
    • Result: Cleaner code and fewer wiring bugs.

    SOLID Alignment

    • D (DIP): Classes depend on interfaces and are wired via configuration.

    Code Example

    @Component
    public class ReportService {
    
        @Autowired
        private DataSource dataSource;
    
        public void generate() {
            System.out.println("Generating report using " + dataSource);
        }
    }
    

    10. Profile and Environment Support

    • Situation: Your app needs different behavior for dev, test, and production.
    • Task: Isolate configuration per environment.
    • Action: Use Spring’s @Profile.
    • Result: Clean environment separation.

    SOLID Alignment

    • O (OCP): Add new environments without changing the core logic.

    Code Example

    @Profile("dev")
    @Component
    public class DevMailClient implements MailClient {
        public void sendEmail(String to, String subject, String body) {
            System.out.println("DEV MODE - Email to " + to);
        }
    }
    

    Conclusion

    IoC isn’t just a theoretical concept — it’s a practical toolkit that solves real-world Java development challenges. These 10 features, when combined with SOLID principles, give you the power to build software that’s maintainable, flexible, and testable.

    Each feature has its place — and now you have a guide to applying them with real-world clarity.

    More information about and example can be found here

  • Structuring Spring Boot Applications

    Structuring Spring Boot Applications

    Designing a Clean Architecture with Controller, Mapper, Service, and Repository

    When building modern Spring Boot applications, organizing code in layers brings clarity, separation of concerns, and easier testing. One of the most common and effective approaches is using the Controller → Service → Mapper → Repository flow.

    In this post, I’ll walk you through how we typically handle create, update, and delete operations using this layered strategy—with code samples.


    1. Create Operation (POST)

    When a client sends a request to create a new resource, here’s how the flow works:

    • Controller: The endpoint is mapped using @PostMapping. We receive the request body as a DTO (e.g., MyEntityDto) and forward it to the service layer.
    • Service: The service receives the DTO and delegates the conversion to a mapper, which transforms the DTO into an entity.
    • Repository: The service calls the repository to persist the new entity.
    • Back to Service: Once saved, the service maps the entity back to a DTO.
    • Controller Response: The controller returns a 201 Created response with the DTO of the newly created resource.

    Controller

    @PostMapping
    public ResponseEntity<MyEntityDto> create(@RequestBody MyEntityDto dto) {
        MyEntityDto created = myService.create(dto);
        return ResponseEntity.status(HttpStatus.CREATED).body(created);
    }
    

    Service

    public MyEntityDto create(MyEntityDto dto) {
        MyEntity entity = mapper.toEntity(dto);
        MyEntity saved = repository.save(entity);
        return mapper.toDto(saved);
    }
    

    Mapper (MapStruct)

    @Mapper(componentModel = "spring")
    public interface MyEntityMapper {
        MyEntity toEntity(MyEntityDto dto);
        MyEntityDto toDto(MyEntity entity);
    }
    

    2. Update Operation (PATCH)

    When a partial update is requested:

    • Controller: We map a @PatchMapping, usually with the resource ID as a path variable and the incoming updates as a DTO in the request body.
    • Service: The service retrieves the existing entity from the database.
    • Mapper: Instead of replacing the entity, we patch it — the mapper applies non-null values from the DTO onto the existing entity.
    • Repository: The patched entity is saved.
    • Back to Service: The saved entity is then mapped to a DTO.
    • Controller Response: The updated DTO is returned with a 200 OK.

    Controller

    @PatchMapping("/{id}")
    public ResponseEntity<MyEntityDto> update(@PathVariable Long id, @RequestBody MyEntityDto dto) {
        MyEntityDto updated = myService.patch(id, dto);
        return ResponseEntity.ok(updated);
    }
    

    Service

    public MyEntityDto patch(Long id, MyEntityDto dto) {
        MyEntity entity = repository.findById(id)
            .orElseThrow(() -> new EntityNotFoundException("Entity not found"));
        
        mapper.patch(dto, entity); // modifies the existing entity
        MyEntity saved = repository.save(entity);
        return mapper.toDto(saved);
    }
    

    Mapper (MapStruct with @MappingTarget)

    @Mapper(componentModel = "spring")
    public interface MyEntityMapper {
        MyEntity toEntity(MyEntityDto dto);
        MyEntityDto toDto(MyEntity entity);
    
        void patch(MyEntityDto dto, @MappingTarget MyEntity entity);
    }
    

    3. Delete Operation (DELETE)

    Deletion is straightforward:

    • Controller: We use @DeleteMapping with the resource ID.
    • Service: The service can either delete directly using the ID or check if the entity exists before deleting.
    • Repository: The deletion is executed.
    • Controller Response: A 204 No Content response indicates successful deletion.

    Controller

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        myService.delete(id);
        return ResponseEntity.noContent().build();
    }
    

    Service

    public void delete(Long id) {
        if (!repository.existsById(id)) {
            throw new EntityNotFoundException("Entity not found");
        }
        repository.deleteById(id);
    }
    

    Summary

    This architecture keeps responsibilities well-separated:

    • Controller: Handles HTTP-specific concerns.
    • Service: Handles business logic and flow coordination.
    • Mapper: Handles conversion between DTOs and entities.
    • Repository: Talks directly to the database.

    This layered approach not only leads to cleaner code but also makes it easier to maintain, test, and scale.

    Testing Tips for Each Layer

    A clean architecture not only makes development easier—it also enables layered testing. By testing each layer in isolation, you can catch bugs early, simplify debugging, and improve confidence during refactoring.

    Here’s how to approach testing for each part of the stack:


    1. Testing the Controller (Web Layer Test)

    Use @WebMvcTest to test only the controller layer, mocking the service.

    @WebMvcTest(MyController.class)
    class MyControllerTest {
    
        @Autowired
        private MockMvc mockMvc;
    
        @MockBean
        private MyService myService;
    
        @Test
        void testCreate_shouldReturn201() throws Exception {
            MyEntityDto inputDto = new MyEntityDto("Example");
            MyEntityDto outputDto = new MyEntityDto("Example", 1L);
    
            Mockito.when(myService.create(any())).thenReturn(outputDto);
    
            mockMvc.perform(post("/entities")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(asJsonString(inputDto)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").value(1L));
        }
    
        private static String asJsonString(Object obj) throws JsonProcessingException {
            return new ObjectMapper().writeValueAsString(obj);
        }
    }
    

    Tips:

    • Use MockMvc to simulate HTTP requests.
    • Mock the service layer to isolate controller logic.
    • Validate HTTP status and response content.

    2. Testing the Service Layer

    Use unit tests with mocked repository and mapper.

    @ExtendWith(MockitoExtension.class)
    class MyServiceTest {
    
        @InjectMocks
        private MyService myService;
    
        @Mock
        private MyRepository repository;
    
        @Mock
        private MyEntityMapper mapper;
    
        @Test
        void testCreate_shouldReturnDto() {
            MyEntityDto dto = new MyEntityDto("Test");
            MyEntity entity = new MyEntity("Test");
            MyEntity saved = new MyEntity("Test", 1L);
            MyEntityDto resultDto = new MyEntityDto("Test", 1L);
    
            Mockito.when(mapper.toEntity(dto)).thenReturn(entity);
            Mockito.when(repository.save(entity)).thenReturn(saved);
            Mockito.when(mapper.toDto(saved)).thenReturn(resultDto);
    
            MyEntityDto result = myService.create(dto);
            assertEquals(1L, result.getId());
        }
    }
    

    Tips:

    • Mock all dependencies (repository, mapper).
    • Focus on business logic and flow.
    • Assert results and interactions.

    3. Testing the Mapper (MapStruct)

    If using MapStruct, it’s usually safe to trust the code generation, but you can still write basic tests.

    class MyEntityMapperTest {
    
        private final MyEntityMapper mapper = Mappers.getMapper(MyEntityMapper.class);
    
        @Test
        void testToDto_shouldMapCorrectly() {
            MyEntity entity = new MyEntity("Test", 1L);
            MyEntityDto dto = mapper.toDto(entity);
            assertEquals("Test", dto.getName());
            assertEquals(1L, dto.getId());
        }
    }
    

    Tips:

    • Use MapStruct’s Mappers.getMapper(...) for standalone tests.
    • Test edge cases (nulls, empty strings, etc.).

    4. Testing the Repository

    Use @DataJpaTest to run tests against an in-memory database (H2 by default).

    @DataJpaTest
    class MyRepositoryTest {
    
        @Autowired
        private MyRepository repository;
    
        @Test
        void testSaveAndFind() {
            MyEntity entity = new MyEntity("Test");
            MyEntity saved = repository.save(entity);
    
            Optional<MyEntity> found = repository.findById(saved.getId());
            assertTrue(found.isPresent());
            assertEquals("Test", found.get().getName());
        }
    }
    

    Tips:

    • Focus on persistence behavior (save, find, delete).
    • Use H2 for fast, isolated tests.
    • Rollback after each test automatically with @DataJpaTest.

    Final Thoughts

    Testing each layer separately brings peace of mind during development. Combined with the clean architecture of Controller → Service → Mapper → Repository, you can build robust, maintainable applications with confidence.

  • Mastering entity relation mapping in Java

    Mastering entity relation mapping in Java

    Parent Perspective in Entity Mapping

    Have you ever asked: “Should the parent own the relation, with cascade all, and the child DTO not even mention the parent?”

    That’s often a good and clean approach, and here’s why.


    When the Parent Owns the Relation

    @Entity
    public class Person {
        @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
        @JoinColumn(name = "passport_id")
        private Passport passport;
    }
    

    Pros:

    • Clear lifecycle control: When you persist or delete the parent, the child follows.
    • Simpler DTOs: Child DTO doesn’t need a back-reference to parent, reducing nesting.
    • Works great when child is tightly coupled to the parent (e.g., Passport can’t exist without Person).

    DTO Example (clean):

    public record PersonDto(Long id, String name, PassportDto passport) {}
    public record PassportDto(String number, LocalDate issueDate) {}
    

    No need for PersonDto inside PassportDto.


    When to Use This (and Cascade ALL)

    • Child only exists with the parent
    • You’re creating/updating data mostly via parent
    • You want to reduce back-reference clutter and circular mapping
    • You don’t care about child-to-parent navigation (e.g., passport.getPerson())

    ⚠️ When the Child Should Keep a Parent Reference

    @Entity
    public class Passport {
        @OneToOne
        @JoinColumn(name = "person_id")
        private Person person;
    }
    

    Use only when:

    • Child needs context of parent (e.g., audit, filtering, backrefs)
    • You often load children independently and need to reach the parent
    • You’re modeling a bidirectional graph for complex navigation

    In DTOs, this can lead to recursive structures or unnecessary nesting:

    public record PassportDto(String number, LocalDate issueDate, PersonDto person) {}
    

    Which can get ugly, especially in JSON responses.


    Recommended Clean Design

    Let the parent own the relationship, use cascade + orphan removal, and avoid child-to-parent references unless strictly necessary.

    This way:

    • Your APIs stay flat and clean.
    • Your mappers don’t loop.
    • You don’t overfetch or serialize unintended stuff.

    Bonus: @JsonIgnore and Mapping

    If you must have a bidirectional entity model, you can still keep DTOs clean:

    @Entity
    public class Passport {
        @JsonIgnore
        @OneToOne(mappedBy = "passport")
        private Person person;
    }
    

    A test poc implementation can be found here.

  • Smarter Switch Statements

    Smarter Switch Statements

    in Java 21 with Pattern Matching

    Java has come a long way since the days of switch (int) and switch (String). With Java 21, the switch statement becomes way more expressive thanks to pattern matching — allowing it to work seamlessly with types, records, sealed hierarchies, and more.

    In this post, we’ll explore what’s new in switch, how it improves readability, safety, and power, and see it in action with real examples.


    Why Pattern Matching in Switch?

    Traditionally, handling polymorphic logic in Java looked something like this:

    if (obj instanceof String s) {
        System.out.println(s.toUpperCase());
    } else if (obj instanceof Integer i) {
        System.out.println(i + 1);
    }
    

    Now with pattern matching in switch, you get a cleaner, more concise version:

    Object obj = "Hello";
    
    switch (obj) {
        case String s -> System.out.println("String: " + s.toUpperCase());
        case Integer i -> System.out.println("Integer + 1: " + (i + 1));
        case null -> System.out.println("It's null!");
        default -> System.out.println("Unknown type");
    }
    

    Guards with when Clauses

    Pattern matching also supports guard conditions using when:

    Object obj = 42;
    
    switch (obj) {
        case Integer i when i > 100 -> System.out.println("Large integer: " + i);
        case Integer i -> System.out.println("Small integer: " + i);
        default -> System.out.println("Other type");
    }
    

    This allows more nuanced branching without breaking type safety or resorting to deeply nested if statements.


    Records + Sealed Interfaces: A Perfect Match

    Let’s bring in some more modern Java tools — records and sealed interfaces — to see the real power of pattern matching.

    1. Define a Sealed Interface

    sealed interface Notification permits Email, SMS {}
    
    record Email(String from, String subject, String body) implements Notification {}
    record SMS(String number, String message) implements Notification {}
    

    2. Handle Logic with Pattern Matching

    Notification n = new Email("support@example.com", "Welcome", "Hello!");
    
    String result = switch (n) {
        case Email e when e.subject().contains("Welcome") ->
            "Send welcome pack to: " + e.from();
        case Email e ->
            "Archive email from: " + e.from();
        case SMS s when s.message().contains("URGENT") ->
            "High priority SMS to: " + s.number();
        case SMS s ->
            "Standard SMS to: " + s.number();
    };
    

    The compiler knows Notification can only be an Email or SMS, so the switch is exhaustive — no need for a default case, and the compiler ensures you cover all branches.


    Bonus: Enum Matching Still Shines

    While switch on enums isn’t new, you can now use when with them too:

    enum Status { ACTIVE, INACTIVE, ERROR }
    
    Status status = Status.ACTIVE;
    
    String message = switch (status) {
        case ACTIVE -> {
    		    	if (random) yield "Randomly excited!";
    		    	else yield "Just active.";
    		    }
        case INACTIVE, ERROR -> "Not working.";
    };
    

    Why You Should Care

    The new switch:

    • Improves readability: Less boilerplate, fewer casts.
    • Boosts safety: Exhaustiveness checks + strong typing.
    • Works seamlessly with modern Java features like records and sealed types.

    It’s one of those quality-of-life upgrades that make writing Java code less verbose and more expressive — especially for domain modeling.


    Conclusion

    Java 21’s pattern matching for switch is a big step forward. Whether you’re working with polymorphic models, building REST APIs, or writing DSLs, this feature will make your code cleaner and safer.

    If you haven’t tried it yet — fire up a Java 21 project and give your old switch statements a makeover.

    Here’s a implementation example.

  • Escape the If Jungle

    Escape the If Jungle

    Modern Conditional Strategies in Java

    Classic Conditionals:

    1. if
    2. if else
    3. if - else if - else
    4. switch
    5. Ternary operator (condition ? a : b)

    Advanced or Alternative Structures:

    1. Map<K, Runnable> or Map<K, Function> – to simulate switch/if chains for behavior
    2. enum with behavior (Strategy Pattern) – enums can define a method differently for each constant
    3. Polymorphism – define different implementations for an interface instead of conditional logic
    4. Command Pattern – store commands in a map or list and execute dynamically
    5. Chain of Responsibility Pattern – each handler decides whether to handle or pass along
    6. Reflection – dynamically invoke methods (not recommended for everyday logic, but possible)
    7. Pattern Matching for switch (Java 21+) – powerful new switch functionality with types

    Now, let’s write a class that demonstrates a few of the more interesting and non-obvious ones: using enum with behavior, map of commands, and polymorphism.

    📦 ConditionalStructuresShowcase.java

    import java.util.HashMap;
    import java.util.Map;
    import java.util.function.Consumer;
    
    public class ConditionalStructuresShowcase {
    
        // Using Enum with Strategy Pattern
        enum Operation {
            ADD {
                @Override
                int apply(int a, int b) {
                    return a + b;
                }
            },
            SUBTRACT {
                @Override
                int apply(int a, int b) {
                    return a - b;
                }
            },
            MULTIPLY {
                @Override
                int apply(int a, int b) {
                    return a * b;
                }
            },
            DIVIDE {
                @Override
                int apply(int a, int b) {
                    return b != 0 ? a / b : 0;
                }
            };
    
            abstract int apply(int a, int b);
        }
    
        // Using Map of commands
        private final Map<String, Runnable> commandMap = new HashMap<>();
    
        public ConditionalStructuresShowcase() {
            commandMap.put("hello", () -> System.out.println("Hello there!"));
            commandMap.put("goodbye", () -> System.out.println("Goodbye, friend!"));
            commandMap.put("surprise", () -> System.out.println("🎉 Surprise! 🎉"));
        }
    
        public void executeCommand(String command) {
            commandMap.getOrDefault(command, () -> System.out.println("Unknown command")).run();
        }
    
        // Using Polymorphism to avoid if-else
        interface Animal {
            void speak();
        }
    
        static class Dog implements Animal {
            public void speak() {
                System.out.println("Woof!");
            }
        }
    
        static class Cat implements Animal {
            public void speak() {
                System.out.println("Meow!");
            }
        }
    
        static class NinjaTurtle implements Animal {
            public void speak() {
                System.out.println("Cowabunga!");
            }
        }
    
        public static void main(String[] args) {
            ConditionalStructuresShowcase showcase = new ConditionalStructuresShowcase();
    
            // Example: enum with strategy
            System.out.println("Enum ADD: " + Operation.ADD.apply(5, 3));
            System.out.println("Enum DIVIDE: " + Operation.DIVIDE.apply(10, 2));
    
            // Example: map of commands
            showcase.executeCommand("hello");
            showcase.executeCommand("surprise");
            showcase.executeCommand("not-a-command");
    
            // Example: polymorphism
            Animal dog = new Dog();
            Animal cat = new Cat();
            Animal turtle = new NinjaTurtle();
    
            dog.speak();
            cat.speak();
            turtle.speak();
        }
    }
    

    This class shows three strong alternatives to conditionals:

    • Replace complex switch logic with enums + abstract methods.
    • Use a map of commands to handle input-based logic.
    • Apply polymorphism instead of big if chains to choose behavior based on object type.

    The java project can be found here.