Design a URL shortener like TinyURL

Designing a URL like TinyURL involves creating a service that takes long URLs and converts them into shorter, more manageable links. When designing the system, consider aspects such as scalability, performance, reliability and maintainability is crucial.

Here's a step-by-step guide to help you design the system:

  1. Define Requirements

    • Core functionality:

      • Accept long URLs and generate short URLs.

      • Redirect short URLs to the original long URLs.

    • Optional features:

      • Analytics (track clicks).

      • User accounts for managing links.

      • Custom short URLs

  2. System Components

    • Frontend

      • User interface: Allows user to input long URLs and retrieve short URLs.

      • Tech stack: HTML, CSS, JavaScript

    • Backend

      • API server: Handles URL shortening requests, redirection and analytics.

      • Tech Stack: Node.js with Express.js

    • Database

      • Data storage: Stores mapping between long and short URLs.

      • Tech stack: NoSQL(MongoDB) or SQL.

    • URL generation Service

      • Unique identifier Generation: Creates unique short URLs using techniques like Base62 encoding or UUIDs.

      • Use a library like nanoid for generating unique IDs.

    • Caching Layer

      • Improve performance: Cache frequent URL mappings.

      • Tech stack: Redis or Memcached.

  3. High - level Architecture

    • User interaction flow

      • Submit long URL: User submits a long URL through the frontend.

      • API request: Frontend sends a request to the backend API to shorten the URL.

      • URL generation: Backend generates a unique short URL and stores the mapping in the database.

      • Return short URL: When a user accesses the URL, the backend looks up the original long URL and redirects the user.

  4. Scalability Considerations

    • Load Balancing

      • Distributed Traffic: use load balancers to distribute incoming traffic across multiple servers.

      • Tech stack : NGINX, AWS ELB.

    • Database Sharding

      • Horizontal scaling: Shard the database to distribute data across multiple instances.

      • Partitioning Strategy: Use consistent hashing or range-based partitioning.

    • Caching

      • Reduce Latency: Cache popular short URL mappings in Redis or Memcached.

      • Invalidate cache: Implement cache invalidation policies to keep data up-to-date.

  5. Building the Application

    • Frontend ( form for URL submission)

    •       <form id="urlForm">
              <input type="text" id="longUrl" placeholder="Enter your long URL here" required>
              <button type="submit">Shorten</button>
            </form>
      
            <div id="result"></div>
      
    • JavaScript for handling form submission:

    document.getElementById('urlForm').addEventListener('submit', async function(event) {
      event.preventDefault();
      const longUrl = document.getElementById('longUrl').value;
      const response = await fetch('/api/shorten', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ longUrl }),
      });
      const result = await response.json();
      document.getElementById('result').innerText = `Short URL: ${result.shortUrl}`;
    });
  • Backend

      const express = require('express');
      const app = express();
      const mongoose = require('mongoose');
      const bodyParser = require('body-parser');
      const { nanoid } = require('nanoid'); // For generating unique IDs
    
      mongoose.connect('mongodb://localhost:27017/urlshortener', {
        useNewUrlParser: true,
        useUnifiedTopology: true,
      });
    
      const urlSchema = new mongoose.Schema({
        longUrl: String,
        shortUrl: String,
        createdAt: { type: Date, default: Date.now },
        clickCount: { type: Number, default: 0 },
      });
    
      const URL = mongoose.model('URL', urlSchema);
    
      app.use(bodyParser.json());
    
      app.post('/api/shorten', async (req, res) => {
        const { longUrl } = req.body;
        const shortUrl = nanoid(6); // Generate a 6-character short URL
        const newUrl = new URL({ longUrl, shortUrl });
        await newUrl.save();
        res.json({ shortUrl });
      });
    
      app.get('/:shortUrl', async (req, res) => {
        const { shortUrl } = req.params;
        const urlEntry = await URL.findOne({ shortUrl });
        if (urlEntry) {
          urlEntry.clickCount += 1;
          await urlEntry.save();
          res.redirect(urlEntry.longUrl);
        } else {
          res.status(404).send('URL not found');
        }
      });
    
    1. Mention Additional Features and Enhancements

      Briefly discuss any extra features that could be added.

      Example: We could also add:

      1. Analytics Dashboard: Provide users with insights on link usage.

      2. Custom Short URLs: Allow users to create custom short URLs.

      3. Expiration and Deletion: Implement URL expiry and allow users to delete their short URLs.

  1. Summary

"In summary, designing a URL shortener involves creating a frontend for user interaction, a backend for handling URL generation and redirection, and a database for storing mappings. To ensure scalability, we can use load balancing, caching, and database sharding. Reliability can be maintained through regular backups, monitoring, and security measures. Additional features like analytics and custom short URLs can further enhance the service."