Skip to main content
Sirasudeen P
Software Engineer
View all authors

Building a Scalable Analytics Feature for My URL Shortener

· 3 min read
Sirasudeen P
Software Engineer

"If you can't measure it, you can't improve it."

I've been deep in the code for my URL shortener project ZapLink, and I just wrapped up a super exciting session building out a new feature: analytics.
The tool was already good at its main job—shortening links, but I knew it wouldn't be complete until users could actually see how many people were clicking their links.


1. First, Secure the Data with User Accounts

My first thought was simple: just count the clicks.
But that immediately led to a bigger question: who should get to see the stats?
I definitely didn't want the analytics for a link to be public.

The answer was clear: ZapLink needed user accounts.

Since I'm already using Supabase for my PostgreSQL database, their built-in Auth was a no-brainer.
It fits perfectly. I mapped out a full system for users to:

  • Sign up
  • Log in
  • Have their links tied directly to their account

Getting this right was crucial to make sure all the analytics data would be private and secure.


This was the fun part.
My initial idea was to just add a new row to the database for every single click.

But what happens if a link gets thousands of clicks every minute?

  • My database would be working overtime
  • The whole app would slow down
  • Even the most important function - the URL redirection would start to lag

And no one wants to wait for a link to load.


3. The Solution: A "Batch and Flush" Architecture

Here’s the two-step plan I came up with.

Step 1 : The Super-Fast Tally Counter (Redis)

I'm already using Redis for caching because it's lightning-fast.
Now, when a link gets clicked, I don’t touch the main database.
I simply tell Redis to add +1 to a counter for that specific link.
This is practically instant.

Step 2 : The Little Helper Robot (My "Cron Job" Lambda)

I built a separate little worker on AWS Lambda with only one job:

  1. Every 12 hrs, it wakes up (Will provide real-time analytics for paid users in future)
  2. Grabs all new click counts from Redis
  3. Saves them to PostgreSQL in one quick batch
  4. Resets the counters in Redis

Result:
The main database gets a gentle nudge every few minutes instead of being bombarded.
The links stay fast, and I still get perfect analytics.


So, What I Got Done Today

It was a busy one! I managed to:

  • Design the database tables for user links and their analytics
  • Implement a full user login and signup system on the frontend
  • Build the backend worker function that handles all the analytics data
  • Sketch out a cool design for the user dashboard and the analytics page

What's Next?

The backend foundation is all set up and ready to go.
The only thing left is to make it look great.

Tomorrow’s plan:

  • Style the dashboard
  • Style the analytics page
  • Make the new login/signup buttons match the rest of ZapLink

Thanks for reading! It's been an awesome ride so far. Stay tuned for the next update!

How I Wrestled a Custom Domain onto AWS Lambda (and Almost Lost My Mind)

· 3 min read
Sirasudeen P
Software Engineer

Hey everyone,

Today, I wanted to share a recent adventure (and occasional frustration) I had while trying to connect a custom domain name to a serverless function running on AWS Lambda, specifically using a Lambda Function URL. While serverless promises simplicity, getting everything to work just right can throw you some unexpected curveballs.

The Goal

I wanted my custom subdomain. Let’s call it zap.siras.dev , to serve my Lambda-based backend. The backend’s job was simple:

Build a URL shortener... something quick, serverless, and clever.

AWS has this neat thing called Lambda Function URLs, which let you expose your Lambda functions as HTTP endpoints... no API Gateway needed. So I deployed it using that.

It worked great, until I tried to hook it up with a custom domain.

The Brick Wall: Lambda Function URL + API Gateway

To connect my domain, I went to API Gateway, expecting to just choose my Lambda function from a dropdown.

But it was empty. Nothing. Nada.
Lambda Function URLs were nowhere to be found.

The Great Debug Hunt

So I started investigating. Here’s what I checked:

  • Same Region: Both my Lambda function and API Gateway were in ap-south-1.
  • Public Access: AuthType set to NONE.
  • Lambda was Live: It worked with its direct AWS URL.
  • Tried Different Browsers: Chrome, Firefox, incognito... no luck.
  • Even Made a New Lambda Function: Still didn’t show up.

It was baffling.

Switching Gears: Trying HTTP API Gateway

Thinking maybe Function URLs were too new or incompatible, I pivoted to HTTP API Gateway.

But again, same issue when attaching the custom domain.
The Lambda list was still empty. My hands were tied.

Plan C: Using REST API Gateway (Old but Gold)

Desperate, I gave REST API Gateway a try.

Here’s what I did:

  • Created a REST API with a {proxy+} path.
  • Set up Lambda Proxy Integration.
  • Connected it to my Lambda function.

And finally... 🎉
The custom domain configuration worked! The REST API showed up, ready to be mapped.

Connecting the Domain via Route 53

Next, I set up my DNS in Route 53:

  • Added a record for zap.siras.dev.
  • Pointed it to the API Gateway endpoint.

Then I hit the browser... and got:

“Site can’t be reached”

DNS takes time to propagate. I waited, flushed DNS (ipconfig /flushdns), and eventually...

⚠️ “Missing Authentication Token”

Strangely, this was good news! 😂 It meant AWS was now receiving my requests.

Final Fixes: CORS, Auth, and Path Madness

I tweaked a few settings:

  • Set Authorization to NONE.
  • Made sure API Key Required was disabled.

Then I tested zap.siras.dev — it worked!
But zap.siras.dev/test didn’t. Why?

Turns out, my Express app expected /api/test, but the browser sent /test. A quick fix in the routing logic, redeployed the function, and...

Success! Everything Finally Works!

All systems go!

  • zap.siras.dev → ✅
  • /:shortUrl style paths → ✅
  • Lambda + API Gateway + Route 53 → ✅

I stood up, stretched my arms, and smiled.

Lessons I Learned

  1. Don't Give Up - The simplest solution may not always work, but there is a path forward.
  2. Lambda Function URLs are Limited - They’re great for fast prototypes, but not fully integrated with API Gateway.
  3. REST API Still Rules for Flexibility - Even if it’s more complex.
  4. Every Error is a Clue - Don’t fear them. Embrace and debug them.

Final Thoughts

Serverless can feel like magic when it works. But when it doesn't, it can feel like you're chasing shadows. If you're going through something similar, I hope this post saves you hours of frustration.

Keep building. Keep learning.

Understanding SOLID Principles in Low-Level Design

· 3 min read
Sirasudeen P
Software Engineer

"Good software is understandable, extensible, and maintainable. That's exactly what SOLID aims to achieve."

As I started diving into Low-Level Design (LLD) through TUF+ and hands-on practice, one of the first core topics I focused on was the SOLID principles, the five fundamental guidelines that help make object-oriented design cleaner, more maintainable, and more scalable.

Here’s a simple breakdown of what I learned:

🔹 1. Single Responsibility Principle (SRP)

A class should have one and only one reason to change.

Meaning: Each class should do only one job, not multiple.

Example:
Instead of one Employee class doing database and email work, I split it into:

  • Employee – holds data
  • EmployeeRepository – handles DB logic
  • EmailService – sends mails

This makes each class smaller, testable, and focused.

🔹 2. Open/Closed Principle (OCP)

Classes should be open for extension but closed for modification.

Meaning: You should be able to add new features without changing existing code.

Example:
Instead of a Shape class using if conditions for area logic, I used a Shape interface and let each shape (Circle, Rectangle) implement its own area() method.

When I added a Triangle, no existing code broke. I just added a new class.

🔹 3. Liskov Substitution Principle (LSP)

Subclasses should be substitutable for their base classes.

Meaning: Derived classes must behave as expected when used as base types.

Anti-Example: Making an Ostrich extend Bird and overriding fly() with an exception violates LSP.

Fix: I split Bird into Bird (for common behavior) and FlyingBird, so Ostrich doesn’t pretend to fly.

🔹 4. Interface Segregation Principle (ISP)

Clients should not be forced to depend on interfaces they don't use.

Meaning: Split large interfaces into smaller ones.

Anti-Example: A Worker interface with both work() and eat() but robots don’t eat!

Fix: Created separate Workable and Eatable interfaces. Now robots implement only what they need.

🔹 5. Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions.

Meaning: Code to interfaces, not concrete classes.

Bad: App directly depends on MySQLDatabase.

Good: Created a Database interface and passed MySQLDatabase via constructor — now it’s easy to switch to MongoDB or mock in tests.


Final Thoughts

Learning and applying SOLID has transformed how I write code. I used to just make things “work” and now I try to make them clean, modular, and extendable.

Next, I’m going to explore:

  • Design Patterns (Factory, Strategy, Observer, etc.)
  • Real-world LLD case studies like Splitwise & Parking Lot
  • Class & Sequence diagrams for better system modeling

You can check out my GitHub repo where I’m storing all my code examples 👉 lld-patterns GitHub


Thanks for reading! If you’re also learning LLD, feel free to reach out or suggest improvements.