The Backend Onion šŸ§…

m13ham13ha
4 min read

One of the perks of working as a Software Developer at my current job is that I’ve gotten to see software development from a bunch of different angles. I’ve built stuff, shipped stuff, watched stuff break, and fixed it again. I’ve seen how infrastructure, while often behind the scenes, quietly shapes the cost and design of everything we build. But the part I’ve probably enjoyed the most? Learning how to structure large-scale backend systems. And that’s exactly what this piece is about.

So… What’s a ā€œLayerā€?

Let’s back up a bit.

Think of software like an onion. Not because it makes you cry (although… sometimes šŸ‘€), but because it has layers. Frontend, backend, database, infrastructure—all stacked together. But even within those broad categories, there are more layers hiding inside.

I used to think the backend was just APIs and a database. Request comes in, we fetch some stuff, maybe save some stuff, return a response—done. But over time, I realised there’s a better way to break things up. Just like the frontend has things like components or MVC (model-view-controller), the backend also benefits from a layered structure.

Here’s the version that’s stuck with me the most: API → Service → Repository.

Let’s peel each one open.


šŸ›£ļø API Layer

This is what talks directly to the outside world—your routes, your endpoints. It's where the frontend (or any external tool) makes a request. The API layer’s job is simple:

  • Handle routing

  • Authenticate the request

  • Validate input

  • Pass things along to the Service Layer

This layer should stay clean. No business logic here. Just making sure things are secure, valid, and sent to the right place.


🧠 Service Layer

This is where your app's brain lives.

All the business logic, the transformations, the ā€œwhat should happen when X is true and Y is falseā€ rules go here. You want to update a user’s profile picture? This is where the magic happens.

Need to call multiple repositories? Talk to an external service? Fire off an event? All that happens here.

And if you need to save or fetch anything, you call...


šŸ’¾ Repository Layer

This layer handles all things database. No fancy logic. Just pure CRUD:

  • Save data

  • Fetch data

  • Update data

  • Delete data

Keeping it separate makes things easier to test and reuse. It’s like your backend’s personal librarian—knows where everything is, doesn’t mess with it.


šŸ“œ A Real-ish Example

Here’s what this looks like in a less structured setup. Imagine this all inside one giant API function:

func UpdateUsernameHandler(w http.ResponseWriter, r *http.Request) {
    // Auth check
    userID := r.Header.Get("X-User-ID")
    if userID == "" {
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return
    }

    // Parse request
    var req struct {
        Username string `json:"username"`
    }
    err := json.NewDecoder(r.Body).Decode(&req)
    if err != nil || req.Username == "" {
        http.Error(w, "Invalid request", http.StatusBadRequest)
        return
    }

    // Update DB
    db.Exec("UPDATE users SET username = ? WHERE id = ?", req.Username, userID)

    // Return response
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("Username updated"))
}

Now, this works, but imagine repeating that for every route. It becomes this spaghetti mess of auth checks, validation, business logic, and DB stuff all mashed together.


🧼 Cleaner Version (With Layers)

Let’s split it up.

API Layer:

func UpdateUsernameHandler(w http.ResponseWriter, r *http.Request) {
    userID := GetUserIDFromHeader(r)
    if userID == "" {
        respondUnauthorized(w)
        return
    }

    req, err := ParseAndValidateUsernameRequest(r)
    if err != nil {
        respondBadRequest(w, err)
        return
    }

    err = userService.UpdateUsername(userID, req.Username)
    if err != nil {
        respondServerError(w, err)
        return
    }

    respondSuccess(w, "Username updated")
}

Service Layer:

func (s *UserService) UpdateUsername(userID, newUsername string) error {
    if len(newUsername) < 3 {
        return errors.New("username too short")
    }

    // Optional: check if username already taken
    exists, _ := s.repo.DoesUsernameExist(newUsername)
    if exists {
        return errors.New("username already exists")
    }

    return s.repo.UpdateUsername(userID, newUsername)
}

Repository Layer:

func (r *UserRepo) UpdateUsername(userID, newUsername string) error {
    _, err := r.db.Exec("UPDATE users SET username = ? WHERE id = ?", newUsername, userID)
    return err
}

func (r *UserRepo) DoesUsernameExist(username string) (bool, error) {
    var count int
    err := r.db.QueryRow("SELECT COUNT(*) FROM users WHERE username = ?", username).Scan(&count)
    return count > 0, err
}

Why Bother?

This way of writing code is called Separation of Concerns, and once it clicks, it’s hard to go back.

Breaking things into layers helps you:

  • Reuse code (validation, auth, DB access)

  • Write smaller, focused functions

  • Test easily (mock a service, test a repo)

  • Onboard teammates without overwhelming them

  • Debug faster

Each layer has its own job. No one’s stepping on anyone else’s toes. You can evolve and scale your app without turning it into an unmaintainable nightmare.


Final Thoughts

This isn’t some strict rule. But once you start building bigger apps with more moving parts, splitting your code into layers will save your sanity. Just like onions add flavour to food (and tears to your eyes), backend layers add structure to your code (and prevent future crying during refactors šŸ˜…).

If you’ve ever struggled to keep things clean in your backend or found your functions growing out of control, give layering a shot.

Your future self will thank you.

0
Subscribe to my newsletter

Read articles from m13ha directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

m13ha
m13ha

Welcome to my little corner of the internet where business savvy meets coding prowess, all wrapped up in the day-to-day adventures of yours truly. Whether you're here to level up your programming skills, gain some business insights, or just enjoy a slice of life through my eyes, you're in the right place! #CodeLife #BusinessMinded #TechEntrepreneur #ProgrammingAdventures #StartupJourney #DeveloperDiaries #LifeInCode #BusinessAndBytes #TechLifeBalance #CodeEntrepreneur