Project Name: Payment Wallet System

Project Goal 

Build a simple Digital Payment Wallet with the following features : 

=> User registration and login 
=> Add money to wallet (top-up)
=> Transfer money to another user 
=> View transaction history 
=> Check current balance 

Tech Stack 

=> Spring Boot 3.x
=> Spring Data JPA 
=> H2 (dev) / PostgreSQL (optional) 
=> Model Mapper 
=> Feign Client (for inter-service communication)
=> Spring Security + JWT (basic)
=> Docker + Docker Compose
=> Maven (multi-module)

Microservices Architecture 

  1. User Service : User registration, login, profile
  2. Wallet Service : Balance management, top-up 
  3. Transaction Service : Record transfers, history 
  4. Notification Service : Simple notification (Email/SMS simulation)

_________________________________________________________________________ 

GitHub URLpayment-wallet-system🔗 

How to start the project ?
 
=> Create a project using Spring initializr (start.spring.io) with basic dependencies 
=> Download the zip file, extract, open in IDE (intelliJ, or any other)
=> Since this is a multi-module maven project, add 4 modules (right click, new -> module) 

In parent pom.xml, 
    => Ensure the packaging pom, not jar
    => We can get required dependencies from maven repo (https://mvnrepository.com/)
    => Ensure the properties java.version, spring-boot.version
    => We cannot have direct dependencies here. We can use this parent pom file for version management only. 
    => So, put all the dependencies in dependencyManagement 
    => For spring-boot-starter dependencies, instead of mentioning everything in the dependencyManagement, we can simply have parent component and mention version. 
    => For this specific project, we needed spring security, jjwt (json web token) api, impl, jackson dependencies and lambok dependencies
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>payment-wallet-system</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>

<name>Payment Wallet System</name>
<description>Microservices based digital payment wallet</description>

<modules>
<module>user-service</module>
<module>wallet-service</module>
<module>transaction-service</module>
<module>notification-service</module>
</modules>

<properties>
<java.version>25</java.version>
<spring-boot.version>3.5.9</spring-boot.version>
</properties>

<!-- Use spring-boot-starter-parent as parent (remove dependencyManagement import) -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.9</version>
<relativePath/>
<!-- lookup parent from repository -->
</parent>

<!-- Dependency versions only (no actual dependencies here) -->
<dependencyManagement>
<dependencies>
<!--jjwt-->
<!-- Source: https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.13.0</version>
</dependency>
<!-- Source: https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.13.0</version>
</dependency>
<!-- Source: https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-jackson -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.13.0</version>
</dependency>

<!-- Source: https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.42</version>
</dependency>

<!-- Source: https://mvnrepository.com/artifact/org.modelmapper/modelmapper -->
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.2.0</version>
</dependency>

</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
In user-service pom.xml, 
    => Ensure the parent is payment-wallet-system
    => Here, we mention actual dependencies without version in dependencies 
    => Note : For Lambok dependency, we must mention <optional>true</optional>
    => For some dependencies, we need to mention scope
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.example</groupId>
<artifactId>payment-wallet-system</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<artifactId>user-service</artifactId>

<!--Actual dependencies import-->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- Jakarta Validation API + Hibernate Validator implementation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<!--spring-boot-starter-security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!--jjwt-->
<!-- Source: https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<!-- Source: https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Source: https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-jackson -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>

<!-- Source: https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<!--lombok - optional – reduces boilerplate-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional> <!--Mention must-->
</dependency>

<!-- Source: https://mvnrepository.com/artifact/org.modelmapper/modelmapper -->
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<scope>compile</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

<properties>
<maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>25</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

</project>
What is the use of @Data annotation on top of DTO and Entity classes ? 
 
@Data is a Lombok annotation that automatically generates a bunch of boilerplate methods for your class, making your code much cleaner and shorter.
 
What @Data Automatically Generates

=> getter methods for all fields
=> setter methods for all non-final fields
=> toString() method (nice formatted string representation)
=> equals() and hashCode() methods (based on all fields)
=> canEqual() (for subclass compatibility)
 
Example Comparison
 
Without @Data (verbose):
 
public class User {
    private Long id;
    private String username;
    private String password;
    private String email;
    private String fullName;
    private String role;

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    // ... 10+ more getters/setters ...

    @Override
    public boolean equals(Object o) { /* complex */ }
    @Override
    public int hashCode() { /* complex */ }
    @Override
    public String toString() { /* complex */ }
}
 
With @Data (clean):
 
@Data
public class User {
    private Long id;
    private String username;
    private String password;
    private String email;
    private String fullName;
    private String role;
}
 
ie. Same functionality, 80% less code. 
 
=> On DTOs: @Data is ideal — no side effects, makes request/response handling easy.     

=> On Entities (User): @Data is fine, but some people prefer @Getter @Setter @ToString(exclude = "password") to avoid exposing sensitive fields in toString().

=> Lombok dependency must be present in pom.xml

If you ever want to be more explicit (e.g., exclude password from toString), you can replace @Data with:

@Getter
@Setter
@ToString(exclude = "password")
@EqualsAndHashCode
public class User { ... }

user-service endpoints flow explanation briefly 

1. User Registers (POST /api/users/register)

2. User Logs In (POST /api/users/login)
    => User gets a JWT token — this is their “login ticket” for future requests.

3. User Calls Protected Endpoint (e.g., GET /api/users/me)

User action:

=> Wants to see their profile
=> Sends request with header:
        Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... 

What happens:

  • Request hits JwtAuthenticationFilter (runs before controller)
    • Checks header → extracts token (after "Bearer ")
    • Uses JwtUtil.extractUsername(token) → gets username
    • If token valid (not expired, signature correct) → loads user details (CustomUserDetailsService)
    • Sets authentication in SecurityContext (marks user as logged in)
    • Passes request to controller
  • Controller (e.g., getCurrentUser) (Controller endpoint /me flow)
    • Gets current username from SecurityContext (who is logged in)
    • Fetches user from DB
    • Returns DTO (profile info)
  • Result:

    => With valid token → 200 OK + profile
    => Without token / invalid token → 401 Unauthorized (Spring Security blocks it)    
     

    In the SecurityFilterChain, why we are having .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

    => We use SessionCreationPolicy.STATELESS specifically because this is a JWT-based authentication system.

    Normal Spring Security Default Behavior (with sessions)

    By default, Spring Security uses session-based authentication:

    • When user logs in → server creates a session ID (stored in cookie or header)

    • Server remembers the user in HttpSession (server memory or Redis)

    • Every future request → browser sends session cookie → server looks up “who is this user?”

    • This is stateful — server keeps state (remembers logged-in users)

    Problems with Stateful Sessions in Modern Microservices / API Systems

    1. Scalability issue

      If you have 10 servers behind a load balancer, each server has its own session memory

      User logs in on server 1 → session created there

      Next request goes to server 2 → server 2 doesn't know the user → logout or re-login required

      Solution? Share sessions via Redis / sticky sessions — complicated & expensive

    2. JWT is designed to be stateless

      JWT token contains all info (username, role, expiration) + is signed

      Server doesn't need to remember anything — just validate signature & expiration

      Any server can handle any request — perfect for microservices & horizontal scaling

    3. Performance & simplicity

      No session storage (Redis, DB) → less cost, less latency

      No session cleanup needed when user logs out

      Works great with mobile apps, SPAs (React, Angular), APIs

    What SessionCreationPolicy.STATELESS does

    .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

  • Tells Spring Security: "Do NOT create or use any HttpSession for authentication."
  • No session cookie is sent/expected
  • Authentication is only via JWT in Authorization header
  • Each request is independent — stateless
  • Summary in simple words

    => We are using JWT tokens → all login info is inside the token itself
    => Server doesn't need to "remember" users → no need for sessions
    => STATELESS mode turns off session creation → makes system scalable, simple, and fast
    => Without this line → Spring would try to create sessions → breaks JWT flow & scalability
    => This is standard for any JWT-based API/microservice project (Many companies use this).

    --------------------------------------------------------------------------------------------------------

    payment-wallet-system deep dive🔗