Sunday, 8 June 2025

Spring Transaction Management:

 


Spring's transaction management is a robust abstraction that simplifies handling database transactions, ensuring data integrity and consistency. 


1. What is Transaction Management?

A transaction is a sequence of operations performed as a single logical unit of work. In Spring, transaction management ensures that either all operations in a transaction succeed (commit) or none do (rollback), maintaining data consistency.

ACID Properties: Atomicity, Consistency, Isolation, Durability


Atomicity:

The @Transactional annotation wraps multiple database operations within a single transaction. If all operations complete successfully, the transaction is committed and changes are saved. If any operation throws a runtime exception (or another specified exception), Spring automatically rolls back the transaction, undoing all changes made during that transaction


Consistency:

This approach ensures that the database remains in a valid state, preventing partial updates or data corruption. If a constraint is violated or an error occurs, the transaction is aborted and the database is restored to its previous state.

Example Workflow:


Start a transaction (@Transactional method is called).


Perform several database operations (e.g., insert, update).


If all succeed, Spring commits the transaction.


If any operation fails (throws an exception), Spring rolls back all changes-none of the operations have any effect on the database

2 . Spring Transaction Management Approaches


Declarative: Using annotations like @Transactional (most common).


Programmatic: Using TransactionTemplate or PlatformTransactionManager directly.


3. Setting Up Transaction Management

Step 1: Add Dependencies


Spring Data JPA or JDBC


Database driver (e.g., H2, MySQL)


Step 2: Enable Transaction Management

@Configuration

@EnableTransactionManagement

public class AppConfig {

    @Bean

    public PlatformTransactionManager txManager(DataSource dataSource) {

        return new DataSourceTransactionManager(dataSource);

    }

}



4.. Using @Transactional Annotation (Declarative Approach)

Basic Usage:

@Service

public class UserService {

    @Autowired

    private UserRepository userRepository;


    @Transactional

    public void createUser(User user) {

        userRepository.save(user);

    }

}


Explanation:

The createUser method runs inside a transaction. If any exception occurs, the transaction is rolled back.


Output:


On successful save: User is persisted.


On exception: No user is saved (rollback).


5. Transaction Propagation

Propagation defines how transactions behave when a transactional method is called by another transactional method. The most common propagation modes are:


REQUIRED (default): Uses the existing transaction if one exists; otherwise, creates a new one.


REQUIRES_NEW: Suspends any existing transaction and starts a new one.


NESTED: Executes within a nested transaction if a current transaction exists (uses savepoints); otherwise, behaves like REQUIRED.

SUPPORTS: Join if exists, else non-transactional.


6. Isolation Levels and Read-Only Transactions


Isolation Levels

Isolation levels control how transaction integrity is maintained when multiple transactions are running concurrently. Common levels include:

READ_UNCOMMITTED: Lowest level; allows dirty reads.

READ_COMMITTED: Default in many databases; prevents dirty reads.

REPEATABLE_READ: Ensures that if a value is read twice in the same transaction, it remains consistent.

SERIALIZABLE: Highest isolation; fully serializes transactions.


7.Exception Handling and Rollback

Unchecked exceptions (RuntimeException): Trigger rollback by default.


Checked exceptions: Do not trigger rollback unless specified.


@Transactional(rollbackFor = IOException.class)

public void method() throws IOException { ... }


Output:

Rolls back on IOException as  well



 How does Spring ensure transactional integrity in a distributed environment ?


Can you provide an example of using @Transactional with multiple database operations ?







Collectors in java8 streams

 


Java 8’s Collectors class provides a suite of static methods for accumulating elements from a stream into collections, strings, maps, and more.


 Collectors are typically used with the Stream.collect() method, making it easy to process and transform data in a functional style.


Why:


Collectors provide ready-made solutions for common reduction operations, such as collecting stream elements into collections, grouping, partitioning, joining, and summarizing data. They make code concise, readable, and expressive.


When:


Use collectors when you want to gather, transform, group, or summarize data from a stream into a desired result format (e.g., List, Set, Map, or a single value).


Common Collectors and Their Use Cases


1. Collectors.toList()

When: 

When you want to collect stream elements into a List.


Example:

List<String> cities = Arrays.asList("Mumbai", "Delhi", "Bangalore", "Chennai");

List<String> cityList = cities.stream().collect(Collectors.toList());

System.out.println(cityList); // Output: [Mumbai, Delhi, Bangalore, Chennai]




Explanation:

Collects all stream elements into a List in encounter order


2. Collectors.toSet()

When:

When you need a Set (no duplicates) from stream elements.


Example:

List<String> cities = Arrays.asList("Mumbai", "Delhi", "Mumbai", "Bangalore");

Set<String> citySet = cities.stream().collect(Collectors.toSet());

System.out.println(citySet); // Output: [Mumbai, Delhi, Bangalore]


Explanation:

Removes duplicates by collecting elements into a Set



Thursday, 15 May 2025

Stream.iterate () vs iterate method

 


Stream.iterate() produces an infinite sequential stream in which each element is generated by applying a function (a unary operator) to the previous element. Because the stream is potentially unbounded, you almost always combine it with a terminal operation such as limit().


1. Generating a Sequence of Numbers

This example starts with zero and adds 1 for each subsequent element. We limit the stream to the first 10 elements.



import java.util.stream.Stream;


public class IterateBasicExample {

    public static void main(String[] args) {

        // Start at 0, with each subsequent element increased by 1.

        Stream<Integer> numbers = Stream.iterate(0, n -> n + 1);

        

        // Limit to the first 10 elements and print each number.

        numbers.limit(10).forEach(System.out::println);

    }

}



Explanation: Here, the initial element is 0. The lambda n -> n + 1 tells the stream to generate the next number by adding one to the current number. Since the stream is infinite, using limit(10) ensures that only 10 numbers are processed.


Output::

0

1

2

3

4

5

6

7

8

9



2 . Generating the Fibonacci Sequence

This example uses Stream.iterate() along with an array to generate pairs representing consecutive Fibonacci numbers. We then map these pairs to extract the first element in each pair (the Fibonacci number).


import java.util.stream.Stream;


public class FibonacciIterate {

    public static void main(String[] args) {

        // Each element in the stream is an array where:

        // - array[0] is the current Fibonacci number,

        // - array[1] is the next Fibonacci number.

        Stream.iterate(new long[]{0, 1}, pair -> new long[]{pair[1], pair[0] + pair[1]})

              .limit(10)              // Get the first 10 Fibonacci pairs.

              .map(pair -> pair[0])   // Extract the first number from each pair.

              .forEach(n -> System.out.print(n + " "));

    }

}



Explanation:


We start with the array {0, 1}.


The lambda pair -> new long[]{pair[1], pair[0] + pair[1]} computes the next Fibonacci pair.


We then limit the stream to 10 elements and map each array to its first element. This produces the Fibonacci numbers.


 Output::

0 1 1 2 3 5 8 13 21 34 


Stream.generate()

 Stream.generate() uses a given supplier (a function without arguments) to produce an infinite stream of values. Unlike iterate(), each new element is produced independently rather than based on the previous element.





1 . Generating a Constant Value

In this example, we create a stream that repeatedly outputs the same string. We then limit the stream so that only five copies are printed


import java.util.stream.Stream;


public class GenerateBasicExample {

    public static void main(String[] args) {

        // Generate a stream where each element is "Hello Java".

        Stream<String> constantStream = Stream.generate(() -> "Hello Java");

        

        // Limit the stream to 5 elements and print them.

        constantStream.limit(5).forEach(System.out::println);

    }

}



Explanation: The supplier (() -> "Hello Java") is called repeatedly to produce each element in the stream. The stream would be infinite if not for limit(5).



Output::

Hello Java

Hello Java

Hello Java

Hello Java

Hello Java



2 .

Generating Random Numbers

This example makes use of the built-in Math.random() method as a supplier to generate random numbers. Again, we limit the stream so it outputs a finite number of elements.


import java.util.stream.Stream;


public class RandomNumbersGenerate {

    public static void main(String[] args) {

        // Generate an infinite stream of random double values.

        Stream<Double> randomNumbers = Stream.generate(Math::random);

        

        // Limit to 5 random numbers and print each.

        randomNumbers.limit(5).forEach(System.out::println);

    }

}



Explanation: Math::random is a method reference that supplies a new random double (between 0.0 and 1.0) each time it is called. The stream is limited to 5 elements.


Output: The output will be 5 random double numbers, for example:

0.23744893145251387

0.8912345123451234

0.11234567890123456

0.6734523981274352

0.5493827459812739



Foreach () vs foreachordered()

 


1 . Overview of forEach()


What It Does: The forEach() method is a terminal operation that accepts a Consumer<T> (a functional interface that performs an action on each element) and applies it to every element in the stream.


Key Point: When used on sequential streams, forEach() iterates over the elements in their encounter order. However, in parallel streams the order in which elements are processed and consumed is not guaranteed. This can mean improved performance but may result in a non-deterministic order of output.


Sequential Example with forEach()

Even though forEach() is designed to be order-agnostic in parallel contexts, when used on a sequential stream it retains the natural ordering:


import java.util.Arrays;

import java.util.List;


public class ForEachSequential {

    public static void main(String[] args) {

        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        

        System.out.println("Using forEach on sequential stream:");

        numbers.stream().forEach(n -> System.out.print(n + " "));

        

        // Expected Output: 1 2 3 4 5

    }

}



Explanation:


A sequential stream created by numbers.stream() processes elements one by one in encounter order.


When printing via forEach(), the output appears as 1 2 3 4 5 since there is no concurrency involved.


2. Overview of forEachOrdered()

What It Does: The forEachOrdered() method is also a terminal operation that accepts a Consumer<T>, but it guarantees that the elements will be processed in the encounter order of the stream—even when the stream is parallel.


Key Point: In a sequential stream, forEachOrdered() behaves the same as forEach(). In parallel streams, however, forEachOrdered() imposes an ordering constraint which may affect performance since it forces the computation to honor the original order.


Sequential Example with forEachOrdered()

import java.util.Arrays;

import java.util.List;


public class ForEachOrderedSequential {

    public static void main(String[] args) {

        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        

        System.out.println("Using forEachOrdered on sequential stream:");

        numbers.stream().forEachOrdered(n -> System.out.print(n + " "));

        

        // Expected Output: 1 2 3 4 5

    }

}


Explanation:


Since the stream is sequential, both forEach() and forEachOrdered() will produce the same output.


3. Difference in Parallel Streams

When you use parallel streams, the difference between forEach() and forEachOrdered() becomes more apparent.

Example: Parallel Stream with forEach() vs. forEachOrdered()

import java.util.Arrays;

import java.util.List;


public class ForEachParallel {

    public static void main(String[] args) {

        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);

        

        System.out.println("Parallel stream using forEach:");

        numbers.parallelStream().forEach(n -> System.out.print(n + " "));

        // Output might be unordered, e.g.: 3 1 4 2 5

        

        System.out.println("\n\nParallel stream using forEachOrdered:");

        numbers.parallelStream().forEachOrdered(n -> System.out.print(n + " "));

        // Expected Output: 1 2 3 4 5 (maintaining encounter order)

    }

}


Explanation:


Using forEach(): The parallel stream allows concurrent processing of the elements. Because there is no guarantee of ordering, the printed output may come in any order (the exact ordering may vary from run to run).


Using forEachOrdered(): Even though the stream is parallel, forEachOrdered() gathers results while respecting the encounter order. In this example, the output is guaranteed to be 1 2 3 4 5



4 . When to Use Which Method

Use forEach() if:


You do not care about the order of processing/printing in parallel streams.


You are willing to trade order consistency for potential performance gains in a parallel environment.


Use forEachOrdered() if:


Maintaining the encounter order is crucial (for example, when the order of logging or processing matters).


You are working with a parallel stream but must preserve the original ordering.