Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
016a238
My approach created
Aug 24, 2026
66f62f0
Added - pom.xml, gitinore
Aug 24, 2026
9301120
Added -configurations , application.yml, application-local.yml , appl…
Aug 24, 2026
f31da66
Added -test configurations application.yml, application-test.yml
Aug 24, 2026
a678fe4
Added -package, controller, service, AppStarter
Aug 24, 2026
373ad6c
Added -package, repository, domain, Entity-UrlDetails, Repository-Url…
Aug 24, 2026
d83ecb2
Fixed - Misplaced logger initialization
Aug 24, 2026
9646bf5
Alias resolver and test is added so that the alias generation is sepe…
Aug 25, 2026
1d2b818
Exceptions are added
Aug 26, 2026
2f967d6
Added the dto,mapper, resolver to generate alias , snowflake id gener…
Aug 26, 2026
a048883
Added service method for save, delete, get by alias , and paginated g…
Aug 26, 2026
a9a989c
Added basic project needs like Documentation, and its configuration
Aug 27, 2026
21b1ea2
Controller and it Tests are added,together with Exception handling
Aug 27, 2026
9d9629a
Adding Exception handling , mapper change to prefix domain
Aug 27, 2026
afa80b9
Adding Service changes to prefix domain
Aug 27, 2026
7074df1
The Service Test changes , cosmetic
Aug 27, 2026
0d4ab33
Fixing the issue - negative values generated during modulus operation
Aug 27, 2026
053e36d
Added the Controller Integration tests
Aug 27, 2026
8021a8a
Fixing junit failure , passing the domain name
Aug 27, 2026
d90df3c
Added Validation for the input parameters and Its Exception handling
Aug 27, 2026
7548fa2
Added the FrontEndUI components
Aug 31, 2026
f9aed01
Added the docker compose to run on docker
Aug 31, 2026
b03baab
Cleanup-Final Touch-1
Aug 31, 2026
2c9e5fe
Adding the full working code,before git copiliot review
Aug 31, 2026
6249eb9
commiting config files
Aug 31, 2026
78004d6
After copilot review
Aug 31, 2026
9755ca0
Added information on how to run the Program
Aug 31, 2026
a4646c4
Added transaction annotation, missed it
Aug 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,26 @@ It should:
- How to build and run locally.
- Example usage (frontend and API).
- Any notes or assumptions.

## How to run the application

Prerequisites:

- Docker (with Docker Compose)
- JDK 21 (required for local backend build/test outside Docker)

- From the repo root, run: docker compose up --build -d
- Open frontend at http://localhost
- Backend API is at http://localhost:8585
- Redis is at localhost:6379 (primarily internal, but exposed)

- Network: defined in docker-compose.yml
- Volume: defined in docker-compose.yml
- Port mappings:
defined per service in docker-compose.yml

## Notes / Assumptions

- In `backend/src/main/resources/application-docker.yml`, Redis host is set to `host.docker.internal` as a default.
- In `docker-compose.yml`, backend sets `SPRING_DATA_REDIS_HOST=redis` (and port `6379`) via environment variables.
- This is intentional and works in Docker Compose because Spring Boot resolves environment variables with higher precedence than values in `application-docker.yml`.
39 changes: 39 additions & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
.kotlin

### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr

### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/

### VS Code ###
.vscode/

### Mac OS ###
.DS_Store
25 changes: 25 additions & 0 deletions backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Stage 1: Build the application
FROM maven:3.9-eclipse-temurin-21-alpine AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests


# Stage 2: Create the runtime image using Java 21
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN addgroup -S spring && adduser -S spring -G spring
RUN mkdir -p /app/data /app/logs
COPY --from=build /app/target/*.jar app.jar

VOLUME /tmp
VOLUME /app/data
VOLUME /app/logs

EXPOSE 8585

ENTRYPOINT ["java", "-Dspring.profiles.active=docker", "-jar", "app.jar"]


118 changes: 118 additions & 0 deletions backend/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
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>

<groupId>com.tpx.urlshort</groupId>
<artifactId>backend</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<openapi-version>2.6.0</openapi-version>
<sqlite-version>3.46.0.0</sqlite-version>
<byte-buddy.version>1.15.0</byte-buddy.version>
</properties>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

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

<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${openapi-version}</version> <!-- Use the latest version compatible with your Boot version -->
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

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

<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>${sqlite-version}</version>
</dependency>

<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-community-dialects</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>

<!-- Standard Unit Tests Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<net.bytebuddy.experimental>true</net.bytebuddy.experimental>
</systemPropertyVariables>
</configuration>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<net.bytebuddy.experimental>true</net.bytebuddy.experimental>
</systemPropertyVariables>
</configuration>
</plugin>

</plugins>
</build>


</project>
26 changes: 26 additions & 0 deletions backend/src/main/java/com/tpx/urlshort/AppStarter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.tpx.urlshort;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class AppStarter implements CommandLineRunner {

private static final Logger logger = LoggerFactory.getLogger(AppStarter.class);

@Value("${spring.application.name:URL Shortener}")
private String appName;

public static void main(String[] args) {
SpringApplication.run(AppStarter.class, args);
}

@Override
public void run(String... args) throws Exception {
logger.info("Application **** {} **** started", appName);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.tpx.urlshort.cache.redis;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.RedisConnectionFailureException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.Optional;

@Service
public class RedisUrlCacheService implements UrlCacheService {

private static final Logger logger = LoggerFactory.getLogger(RedisUrlCacheService.class);

private final StringRedisTemplate redisTemplate;
private final Duration ttl;

public RedisUrlCacheService(StringRedisTemplate redisTemplate,
@Value("${app.cache.ttl-seconds:600}") long ttlSeconds) {
this.redisTemplate = redisTemplate;
this.ttl = Duration.ofSeconds(ttlSeconds);
}

@Override
public Optional<String> get(String alias) {
try {
validateAlias(alias);
String value = redisTemplate.opsForValue().get(buildKey(alias));
return Optional.ofNullable(value);
} catch (IllegalArgumentException e) {
logger.warn("Redis cache get failed for alias '{}': {}", alias, e.getMessage());
return Optional.empty();
} catch (DataAccessException e) {
logger.warn("Redis cache get failed for alias '{}': {}", alias, e.getMessage());
return Optional.empty();
}
}

@Override
public void put(String alias, String actualUrl) {
try {
validateAlias(alias);
if (actualUrl == null || actualUrl.isBlank()) {
throw new IllegalArgumentException("Actual URL must not be null or empty");
}
redisTemplate.opsForValue().set(buildKey(alias), actualUrl, ttl);
} catch (IllegalArgumentException e) {
logger.warn("Redis cache put failed for alias '{}': {}", alias, e.getMessage());
} catch (DataAccessException e) {
logger.warn("Redis cache put failed for alias '{}': {}", alias, e.getMessage());
}
}

@Override
public void evict(String alias) {
try {
validateAlias(alias);
redisTemplate.delete(buildKey(alias));
} catch (IllegalArgumentException e) {
logger.warn("Redis cache evict failed for alias '{}': {}", alias, e.getMessage());
} catch (DataAccessException e) {
logger.warn("Redis cache evict failed for alias '{}': {}", alias, e.getMessage());
}
}

private String buildKey(String alias) {
return "url:" + alias;
}

private void validateAlias(String alias) {
if (alias == null || alias.isBlank()) {
throw new IllegalArgumentException("Alias must not be null or empty");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.tpx.urlshort.cache.redis;

import java.util.Optional;

public interface UrlCacheService {

Optional<String> get(String alias);

void put(String alias, String actualUrl);

void evict(String alias);
}
31 changes: 31 additions & 0 deletions backend/src/main/java/com/tpx/urlshort/config/CorsConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.tpx.urlshort.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig {

private final CorsProperties corsProperties;

public CorsConfig(CorsProperties corsProperties) {
this.corsProperties = corsProperties;
}

@Bean
public WebMvcConfigurer corsConfigurer() {

return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins(corsProperties.getAllowedOrigins().toArray(new String[0]))
.allowedMethods(corsProperties.getAllowedMethods().toArray(new String[0]))
.allowedHeaders(corsProperties.getAllowedHeaders().toArray(new String[0]))
.allowCredentials(corsProperties.isAllowCredentials());
}
};
}
}
Loading