Tag: java

  • ✅ 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.
  • 🚨 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

  • Understanding One-to-One Relationships: Database vs Java (Hibernate)

    Understanding One-to-One Relationships: Database vs Java (Hibernate)

    When modeling One-to-One relationships, you can approach it in several ways depending on the ownership, lifecycle, and data modeling constraints. While Java and Hibernate offer flexibility in how this relationship is implemented at the code level, the root of it lies in database design. So let’s start there.


    One-to-One Relationship Strategies in Database Engineering

    In relational databases, a One-to-One relationship means each row in Table A corresponds to exactly one row in Table B, and vice versa. But there are multiple ways to implement this, depending on the direction and ownership of the relationship.

    1. Foreign Key in Parent Table (Shared Primary Key strategy – reversed)

    • How it works: The parent table (e.g., Person) holds a foreign key to the child table (Passport.id).
    • Pros:
      • Easy to query from parent.
      • Clear ownership.
    • Cons:
      • Implies that the parent cannot exist without the child — or needs NULL values.
      • Foreign key can be NULL if optional.
    CREATE TABLE person (
        id BIGINT PRIMARY KEY,
        name VARCHAR(100),
        passport_id BIGINT UNIQUE,
        FOREIGN KEY (passport_id) REFERENCES passport(id)
    );
    

    2. Child Table Uses Parent’s Primary Key (Shared Primary Key strategy)

    • How it works: The child table (e.g., Passport) uses the same ID as the parent (Person) — usually both primary key and foreign key.
    • Pros:
      • Enforces strict 1:1 relationship.
      • Cleaner data integrity — Passport cannot exist without Person.
    • Cons:
      • Less flexible if the child becomes optional.
      • Harder to manage outside ORMs.
    CREATE TABLE person (
        id BIGINT PRIMARY KEY,
        name VARCHAR(100)
    );
    
    CREATE TABLE passport (
        id BIGINT PRIMARY KEY,
        number VARCHAR(50),
        FOREIGN KEY (id) REFERENCES person(id)
    );
    

    3. Join Table Strategy (Less common for One-to-One)

    • How it works: A third table (e.g., person_passport) holds person_id and passport_id with a unique constraint.
    • Pros:
      • Very flexible and decoupled.
      • If the relation has a tendency to become 1xN or Nx1 or NxN then it’s a good choice because it will make the transition easier.
    • Cons:
      • Overkill unless mapping polymorphic or cross-entity relations.
      • High complexity for something that can be simple.

    Entity Mapping in Hibernate & Java

    Now let’s translate this to Java and Hibernate.

    Shared Primary Key Strategy (Child uses parent’s ID)

    @Entity
    public class Person {
        @Id
        private Long id;
    
        @OneToOne(mappedBy = "person", cascade = CascadeType.ALL)
        private Passport passport;
    }
    
    @Entity
    public class Passport {
        @Id
        private Long id;
    
        @OneToOne
        @MapsId
        @JoinColumn(name = "id")
        private Person person;
    
        private String number;
    }
    
    • @MapsId indicates that Passport shares the same primary key as Person.
    • Very tight coupling — used when lifecycle is tightly bound.

    Foreign Key in Parent Table

    @Entity
    public class Person {
        @Id
        private Long id;
    
        @OneToOne
        @JoinColumn(name = "passport_id")
        private Passport passport;
    }
    
    @Entity
    public class Passport {
        @Id
        private Long id;
        private String number;
    }
    
    • More flexible: Person can exist without Passport.
    • You control fetch/lazy, cascade, optionality more easily.
    • More flexible: Person can exist without Passport.
    • You control fetch/lazy, cascade, optionality more easily.

    What about property with JoinTables?

    Sometimes, your model may evolve and you’d rather keep the base entity clean and extend when needed. For example, instead of putting a Passport inside Person, you could have:

    @Entity
    public class Passport  {
    
        @OneToOne
        @JoinTable(
            name = "person_passport",
            joinColumns = @JoinColumn(name = "person_id"),
            inverseJoinColumns = @JoinColumn(name = "passport_id")
        )
        private Passport passport;
    }
    

    This approach uses:

    • Join Table in JPA.
    • @JoinTable allows modeling more flexible or optional relationships.
    • Useful when only some persons have passport.
    • A smart choice when there’s a possibility that the relationship’s cardinality may change.

    It’s especially powerful when you want to modularize behavior and avoid cluttering the base Person class but makes the entity mapping very complex.


    What About Inheritance with Joined Strategy?

    Sometimes you just want a easier way to navigate the entity properties and treat the two tables as a single entity. For example, instead of putting a Passport inside Person, you could extend it:

    @Entity
    @Inheritance(strategy = InheritanceType.JOINED)
    public class Person {
        @Column
        @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    }
    
    @Entity
    public class Passport extends Person {
    
       @Column
       private String number;
    }
    

    Summary

    • InheritanceType.JOINED allows you to split the data into separate tables for each class in the inheritance hierarchy.
    • The child class has its own table and its joined with the parent class, so the child entity has all properties from the parent as its own.
    • Hibernate will automatically handle the joins when querying entities from the inheritance hierarchy.

    This strategy is useful when you want to follow database normalization principles but be mindful of the performance overhead due to the necessary joins when querying data.


    Final Thoughts: Choosing the Right Strategy

    Your choice of strategy in the database layer directly impacts your entity mapping in Java:

    • Use Shared Primary Keys when the two entities are tightly bound and always created/removed together.
    • Use a Foreign Key in Parent when the child is optional or loosely coupled.
    • Use Join Tables or Inheritance when dealing with complex relationships, partial behaviors, or domain subtypes.

    Choosing the right one-to-one mapping isn’t just about the ORM — it’s about data consistency, flexibility, and how your domain actually behaves.

  • Mastering MapStruct

    Mastering MapStruct

    @Mapping, @AfterMapping, and @MappingTarget in Action

    🧩 Introduction

    In modern Java applications, especially with frameworks like Spring Boot, object mapping is a common task. Whether you’re transforming DTOs into entities or mapping between domain layers, doing this manually is tedious and error-prone.

    MapStruct is a compile-time code generator that simplifies this task—fast, type-safe, and easy to use. In this article, we’ll explore three powerful annotations that give you more control and flexibility:

    • @Mapping
    • @AfterMapping
    • @MappingTarget

    🔧 Setting the Stage: Basic Setup

    Let’s assume we have a simple Spring Boot project with MapStruct configured.

    Dependencies (Maven):

    <properties>
     <java.version>21</java.version>
     <mapstruct.version>1.5.5.Final</mapstruct.version>
     <mvn.compile.version>3.11.0</mvn.compile.version>
    </properties>
    
    <dependency>
      <groupId>org.mapstruct</groupId>
      <artifactId>mapstruct</artifactId>
      <version>${mapstruct.version}</version>
    </dependency>
    <dependency>
      <groupId>org.mapstruct</groupId>
      <artifactId>mapstruct-processor</artifactId>
      <version>${mapstruct.version}</version>
      <scope>provided</scope>
    </dependency>
    
    <!-- and in build tag plugins -->
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>${mvn.compile.version}</version>
      <configuration>
        <annotationProcessorPaths>
          <path>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-processor</artifactId>
            <version>${mapstruct.version}</version>
          </path>
          <!-- Other processors like Lombok can go here -->
        </annotationProcessorPaths>
      </configuration>
    </plugin>

    Make sure annotation processing is enabled in your IDE or build tool.


    ✅ Example Scenario: Mapping User DTO to Entity

    Let’s say you have:

    UserDTO.java

    public record UserDTO(String firstName, String lastName, String email) {}
    User.java
    public class User {
        private String fullName;
        private String email;
        private LocalDateTime createdAt;
        // getters & setters
    }

    🎯 Using @Mapping to Customize Field Mapping

    By default, MapStruct matches fields by name. But when the names don’t match (like firstName + lastNamefullName), you can use @Mapping.

    UserMapper.java

    @Mapper
    public interface UserMapper {
    
        @Mapping(target = "fullName", expression = "java(dto.firstName() + \" \" + dto.lastName())")
        @Mapping(target = "createdAt", ignore = true) // we’ll handle this later
        User toEntity(UserDTO dto);
    }
    

    Here:

    • expression = "java(...)” lets you use Java code directly for custom logic.
    • ignore = true tells MapStruct to skip mapping that field.

    🧪 Enhancing Mapping with @AfterMapping

    Let’s say we want to set createdAt to the current time. This is where @AfterMapping comes in handy.

    @Mapper
    public interface UserMapper {
    
        @Mapping(target = "fullName", expression = "java(dto.firstName() + \" \" + dto.lastName())")
        @Mapping(target = "createdAt", ignore = true)
        User toEntity(UserDTO dto);
    
        @AfterMapping
        default void setCreatedAt(@MappingTarget User user) {
            user.setCreatedAt(LocalDateTime.now());
        }
    }
    
    • @AfterMapping lets you customize the result after the auto-mapping is done.
    • @MappingTarget gives you access to the mapped target object.

    🔁 Reusing Objects with @MappingTarget

    Suppose you’re updating an existing User entity instead of creating a new one:

    @Mapper
    public interface UserMapper {
    
        @Mapping(target = "fullName", expression = "java(dto.firstName() + \" \" + dto.lastName())")
        @Mapping(target = "createdAt", ignore = true)
        void updateEntity(UserDTO dto, @MappingTarget User user);
    }
    
    • This avoids creating a new object.
    • Great for PATCH/update endpoints where you fetch an entity from the DB, then update some fields.

    🧠 Wrap-Up

    These three annotations are the foundation of writing powerful, flexible, and clean mappers with MapStruct:

    AnnotationPurpose
    @MappingCustomize individual field mapping
    @AfterMappingAdd post-processing logic
    @MappingTargetModify an existing object instead of creating a new one

    Mastering these tools helps you avoid boilerplate, improve readability, and maintain a clean domain model.


    📦 Bonus: When Should You Use These?

    • Use @Mapping when field names/types differ or when you want custom logic.
    • Use @AfterMapping for audit fields (createdAt, updatedBy) or conditional logic.
    • Use @MappingTarget when updating existing entities (e.g., in services).