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:
- Title Support: Each shortened URL can have a descriptive title
- Expiration: URLs can have optional expiration dates
- Click Tracking: Monitor how many times each URL is accessed
- User Management: Associate URLs with users
- Validation: URL format validation and duplicate prevention
- REST API: Full CRUD operations via REST endpoints
- 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.