URL shortener service in Java with a title feature

1. Core Domain Classes

// ShortUrl.java
import java.time.LocalDateTime;
import java.util.Objects;
public class ShortUrl {
private String id;
private String originalUrl;
private String title;
private String shortCode;
private LocalDateTime createdAt;
private LocalDateTime expiresAt;
private int clickCount;
private String createdBy;
public ShortUrl() {}
public ShortUrl(String id, String originalUrl, String title, String shortCode, 
LocalDateTime createdAt, LocalDateTime expiresAt, int clickCount, String createdBy) {
this.id = id;
this.originalUrl = originalUrl;
this.title = title;
this.shortCode = shortCode;
this.createdAt = createdAt;
this.expiresAt = expiresAt;
this.clickCount = clickCount;
this.createdBy = createdBy;
}
// Getters and Setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getOriginalUrl() { return originalUrl; }
public void setOriginalUrl(String originalUrl) { this.originalUrl = originalUrl; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getShortCode() { return shortCode; }
public void setShortCode(String shortCode) { this.shortCode = shortCode; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getExpiresAt() { return expiresAt; }
public void setExpiresAt(LocalDateTime expiresAt) { this.expiresAt = expiresAt; }
public int getClickCount() { return clickCount; }
public void setClickCount(int clickCount) { this.clickCount = clickCount; }
public String getCreatedBy() { return createdBy; }
public void setCreatedBy(String createdBy) { this.createdBy = createdBy; }
public void incrementClickCount() {
this.clickCount++;
}
public boolean isExpired() {
return expiresAt != null && LocalDateTime.now().isAfter(expiresAt);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ShortUrl shortUrl = (ShortUrl) o;
return Objects.equals(id, shortUrl.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}

2. Repository Interface

// ShortUrlRepository.java
import java.util.List;
import java.util.Optional;
public interface ShortUrlRepository {
ShortUrl save(ShortUrl shortUrl);
Optional<ShortUrl> findById(String id);
Optional<ShortUrl> findByShortCode(String shortCode);
List<ShortUrl> findAll();
List<ShortUrl> findByUser(String userId);
boolean delete(String id);
boolean existsByShortCode(String shortCode);
}

3. In-Memory Repository Implementation

// InMemoryShortUrlRepository.java
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class InMemoryShortUrlRepository implements ShortUrlRepository {
private final Map<String, ShortUrl> storage = new ConcurrentHashMap<>();
private final Map<String, String> shortCodeToId = new ConcurrentHashMap<>();
@Override
public ShortUrl save(ShortUrl shortUrl) {
if (shortUrl.getId() == null) {
shortUrl.setId(UUID.randomUUID().toString());
}
storage.put(shortUrl.getId(), shortUrl);
shortCodeToId.put(shortUrl.getShortCode(), shortUrl.getId());
return shortUrl;
}
@Override
public Optional<ShortUrl> findById(String id) {
return Optional.ofNullable(storage.get(id));
}
@Override
public Optional<ShortUrl> findByShortCode(String shortCode) {
return Optional.ofNullable(shortCodeToId.get(shortCode))
.map(storage::get);
}
@Override
public List<ShortUrl> findAll() {
return new ArrayList<>(storage.values());
}
@Override
public List<ShortUrl> findByUser(String userId) {
return storage.values().stream()
.filter(url -> userId.equals(url.getCreatedBy()))
.toList();
}
@Override
public boolean delete(String id) {
ShortUrl removed = storage.remove(id);
if (removed != null) {
shortCodeToId.remove(removed.getShortCode());
return true;
}
return false;
}
@Override
public boolean existsByShortCode(String shortCode) {
return shortCodeToId.containsKey(shortCode);
}
}

4. URL Shortening Service

// UrlShortenerService.java
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import java.util.Random;
public class UrlShortenerService {
private final ShortUrlRepository repository;
private final Random random;
private static final String CHARACTERS = 
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
private static final int SHORT_CODE_LENGTH = 6;
public UrlShortenerService(ShortUrlRepository repository) {
this.repository = repository;
this.random = new Random();
}
public ShortUrl createShortUrl(String originalUrl, String title, String userId, Integer expirationDays) {
validateUrl(originalUrl);
String shortCode = generateUniqueShortCode();
LocalDateTime createdAt = LocalDateTime.now();
LocalDateTime expiresAt = expirationDays != null ? 
createdAt.plusDays(expirationDays) : null;
ShortUrl shortUrl = new ShortUrl(
null, originalUrl, title, shortCode, 
createdAt, expiresAt, 0, userId
);
return repository.save(shortUrl);
}
public Optional<String> resolveUrl(String shortCode) {
Optional<ShortUrl> shortUrlOpt = repository.findByShortCode(shortCode);
if (shortUrlOpt.isPresent()) {
ShortUrl shortUrl = shortUrlOpt.get();
if (shortUrl.isExpired()) {
repository.delete(shortUrl.getId());
return Optional.empty();
}
shortUrl.incrementClickCount();
repository.save(shortUrl);
return Optional.of(shortUrl.getOriginalUrl());
}
return Optional.empty();
}
public Optional<ShortUrl> getUrlInfo(String shortCode) {
return repository.findByShortCode(shortCode)
.filter(url -> !url.isExpired());
}
public List<ShortUrl> getUserUrls(String userId) {
return repository.findByUser(userId).stream()
.filter(url -> !url.isExpired())
.toList();
}
public boolean deleteUrl(String shortCode, String userId) {
Optional<ShortUrl> shortUrlOpt = repository.findByShortCode(shortCode);
if (shortUrlOpt.isPresent() && userId.equals(shortUrlOpt.get().getCreatedBy())) {
return repository.delete(shortUrlOpt.get().getId());
}
return false;
}
private void validateUrl(String url) {
if (url == null || url.trim().isEmpty()) {
throw new IllegalArgumentException("URL cannot be null or empty");
}
if (!url.matches("^(https?|ftp)://.*$")) {
throw new IllegalArgumentException("Invalid URL format");
}
}
private String generateShortCode() {
StringBuilder sb = new StringBuilder(SHORT_CODE_LENGTH);
for (int i = 0; i < SHORT_CODE_LENGTH; i++) {
sb.append(CHARACTERS.charAt(random.nextInt(CHARACTERS.length())));
}
return sb.toString();
}
private String generateUniqueShortCode() {
String shortCode;
int attempts = 0;
int maxAttempts = 10;
do {
shortCode = generateShortCode();
attempts++;
if (attempts > maxAttempts) {
throw new RuntimeException("Unable to generate unique short code");
}
} while (repository.existsByShortCode(shortCode));
return shortCode;
}
}

5. REST API Controller (Spring Boot)

// UrlShortenerController.java
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@RestController
@RequestMapping("/api/urls")
public class UrlShortenerController {
private final UrlShortenerService urlShortenerService;
public UrlShortenerController(UrlShortenerService urlShortenerService) {
this.urlShortenerService = urlShortenerService;
}
@PostMapping("/shorten")
public ResponseEntity<?> shortenUrl(@RequestBody ShortenUrlRequest request) {
try {
ShortUrl shortUrl = urlShortenerService.createShortUrl(
request.getOriginalUrl(), 
request.getTitle(),
request.getUserId(), 
request.getExpirationDays()
);
ShortenUrlResponse response = new ShortenUrlResponse(
shortUrl.getId(),
shortUrl.getOriginalUrl(),
shortUrl.getTitle(),
shortUrl.getShortCode(),
shortUrl.getCreatedAt(),
shortUrl.getExpiresAt(),
shortUrl.getClickCount(),
shortUrl.getCreatedBy()
);
return ResponseEntity.ok(response);
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(
Map.of("error", e.getMessage())
);
}
}
@GetMapping("/{shortCode}")
public ResponseEntity<?> redirectToOriginalUrl(@PathVariable String shortCode) {
Optional<String> originalUrl = urlShortenerService.resolveUrl(shortCode);
if (originalUrl.isPresent()) {
Map<String, String> response = new HashMap<>();
response.put("redirectUrl", originalUrl.get());
return ResponseEntity.ok(response);
} else {
return ResponseEntity.notFound().build();
}
}
@GetMapping("/{shortCode}/info")
public ResponseEntity<?> getUrlInfo(@PathVariable String shortCode) {
Optional<ShortUrl> shortUrl = urlShortenerService.getUrlInfo(shortCode);
if (shortUrl.isPresent()) {
return ResponseEntity.ok(shortUrl.get());
} else {
return ResponseEntity.notFound().build();
}
}
@GetMapping("/user/{userId}")
public ResponseEntity<List<ShortUrl>> getUserUrls(@PathVariable String userId) {
List<ShortUrl> urls = urlShortenerService.getUserUrls(userId);
return ResponseEntity.ok(urls);
}
@DeleteMapping("/{shortCode}")
public ResponseEntity<?> deleteUrl(@PathVariable String shortCode, 
@RequestParam String userId) {
boolean deleted = urlShortenerService.deleteUrl(shortCode, userId);
if (deleted) {
return ResponseEntity.ok().build();
} else {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(Map.of("error", "URL not found or access denied"));
}
}
}
// Request/Response DTOs
class ShortenUrlRequest {
private String originalUrl;
private String title;
private String userId;
private Integer expirationDays;
// Getters and Setters
public String getOriginalUrl() { return originalUrl; }
public void setOriginalUrl(String originalUrl) { this.originalUrl = originalUrl; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public Integer getExpirationDays() { return expirationDays; }
public void setExpirationDays(Integer expirationDays) { this.expirationDays = expirationDays; }
}
class ShortenUrlResponse {
private String id;
private String originalUrl;
private String title;
private String shortCode;
private LocalDateTime createdAt;
private LocalDateTime expiresAt;
private int clickCount;
private String createdBy;
public ShortenUrlResponse(String id, String originalUrl, String title, String shortCode, 
LocalDateTime createdAt, LocalDateTime expiresAt, 
int clickCount, String createdBy) {
this.id = id;
this.originalUrl = originalUrl;
this.title = title;
this.shortCode = shortCode;
this.createdAt = createdAt;
this.expiresAt = expiresAt;
this.clickCount = clickCount;
this.createdBy = createdBy;
}
// Getters
public String getId() { return id; }
public String getOriginalUrl() { return originalUrl; }
public String getTitle() { return title; }
public String getShortCode() { return shortCode; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getExpiresAt() { return expiresAt; }
public int getClickCount() { return clickCount; }
public String getCreatedBy() { return createdBy; }
}

6. Configuration and Main Application

// UrlShortenerApplication.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class UrlShortenerApplication {
public static void main(String[] args) {
SpringApplication.run(UrlShortenerApplication.class, args);
}
@Bean
public ShortUrlRepository shortUrlRepository() {
return new InMemoryShortUrlRepository();
}
@Bean
public UrlShortenerService urlShortenerService(ShortUrlRepository repository) {
return new UrlShortenerService(repository);
}
}

7. Usage Examples

API Endpoints:

  • POST /api/urls/shorten - Create short URL
{
"originalUrl": "https://example.com/very-long-url",
"title": "Example Website",
"userId": "user123",
"expirationDays": 30
}
  • GET /api/urls/{shortCode} - Redirect to original URL
  • GET /api/urls/{shortCode}/info - Get URL information
  • GET /api/urls/user/{userId} - Get user's URLs
  • DELETE /api/urls/{shortCode}?userId={userId} - Delete URL

Key Features:

  1. Title Support: Each shortened URL can have a descriptive title
  2. Expiration: URLs can have optional expiration dates
  3. Click Tracking: Monitor how many times each URL is accessed
  4. User Management: Associate URLs with users
  5. Validation: URL format validation and duplicate prevention
  6. REST API: Full CRUD operations via REST endpoints
  7. In-Memory Storage: Easy to replace with database persistence

This implementation provides a solid foundation for a URL shortener service that you can extend with additional features like analytics, custom short codes, or database persistence.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper