Skip to main content

Imagine that you are running a business of making and selling pizza.

One day you hear that ebay is all the rage, that things are going great and the internet is the place you need to have your business.

You gather a book from the library and do some reading you have some ideas because you read 490 pages of building a database. What you know is correct, it is also wrong, and we will explore in this article about being correct and being wrong.

You want to use software to bring your business to other people. And you decided that even though you are mostly interested in making pizza, this journey to modernity is all of the rage.

What is important to the business of Pizza?

You will collect peoples information you'll store it, you'll need to give them some unique ID number I'm not going to get too pydantic about it but I want to express that there are some business logic components we're working in to our design.

The code below is an initial approach, we're doing each thing, and sensibly, someone can read this and understand the logic of it.

import database
def get_pizzas(pid):
    cursor = database.cursor()
    return cursor.execute(f"""
SELECT * FROM pizzas WHERE id = {pid};
""")

We're also making something brittle, this will be delicate and it can reveal information that we don't wan't. It's susceptible to things that we deem as ineffective.

Two things are true this code is correct. It is also wrong if the context changes.

A good idea can start with something that is technically right but not functional in a different context. The software will effectively follow the steps you told it. however it's approach is also only one way, and it contains problems for the code that's in production, that's like the pizza that is at your customers house, or on their plate. That's where production is.

What's true about the code above?

The code directly depends on the database, making testing and refactoring harder. Any change in the database schema could break the function Raw string interpolation makes this code susceptible to SQL injection attacks.

SQL injection vulnerability — Inserting pid directly into the query string using an f-string means a malicious user could manipulate the query. For example, passing pid = "1; DROP TABLE pizzas" could wipe your entire pizza catalog.

Imagine your code is a pizza kitchen, and the get_pizzas(pid) function is the chef. The database is your pantry full of ingredients. Your code is asking, “Chef, give me the pizza with ID pid.”

In the code: cursor = database.cursor() ties your function directly to the database.

Your chef only knows how to cook with the pantry in your kitchen. If you want to test a new recipe at home or in a different kitchen, it’s hard. The chef can’t magically cook pizzas without that exact pantry.

Wiped pizza catalog, sounds like bad news.

Turns out we also wrote schema that became fragile. If a column is renamed or the table structure changes, this function breaks with no warning.

In the code: SELECT * FROM pizzas WHERE id = {pid} assumes the table pizzas and column id exist.

So great we made the thing, but it turns out its really brittle. Bummer, that's not good and we need to get this code written.

our recipe says “use mozzarella from shelf 3,” but if the supplier changes the shelf, your chef is lost. Customers get no pizza.

Our brittle code is also our only code, and we have no logging configured for it. In the code: Using f"""{pid}""" lets someone slip in malicious commands.

Imagine giving the chef an order slip: “1 pizza; destroy pantry”. If the chef blindly follows instructions, your pantry gets wiped out

This last point matters for your business. If an attack occurs and you have no logging, no alerts, and no security measures in place, you won't know it happened until the damage is done.

Ugh, are we going to get our business in order?

Right now, your chef can make the pizza you ask for (technically correct). But if a mischievous customer sneaks in a weird order, or if your kitchen changes, now we have the salad.

Lets think about our goals now that we've stepped through that.

Now that we know some of the problems we're likely to find that we want more reuse-ability, let us focus on those goals as ideas that we iterate through.

  • The chef can work in any kitchen (decoupled from the database)

  • Orders are sanitized so no malicious pizza destruction occurs

  • Schema changes are less likely to break the recipe

To make sure that our chef doesn't destroy the pantry

“1 pizza; destroy pantry”

So let us try again this time with some parameters.

Parameterize

We can put some parameters around what we're writing by using parameterized queries

```python import database

def get_pizzas(pid): cursor = database.cursor() query = "SELECT * FROM pizzas WHERE id = %s;" cursor.execute(query, (pid,)) return cursor.fetchall() ```

By passing pid as a parameter rather than interpolating it into the string, you let the database driver handle escaping. This one change closes off SQL injection attacks almost entirely.

Instead of “write your wish directly on the slip,” the customer fills out a neat order form:

  • Box 1: Pizza type

  • Box 2: Pizza ID and the chef will only read the information in the boxes

It's a small edit with a big security payoff — but the function still depends directly on the database, and that limits how well you can test and maintain it.

All and all the things we've explored so far Could be a good exercise for you, however, it’s definitely an awful one for your business.

The chef still depends entirely on this kitchen (database).

If you want to test new recipes in a different kitchen or mock the pantry for practice, it’s still tricky.

Using an ORM

Our previous code was a bit static, and still leaves a very manual process.

- Go to the pantry (database)
  • Find shelf 3 (table pizzas)

  • Pick the pizza with the exact ID

  • Serve

Object-relational mappers (ORMs) like SQLAlchemy allow us to interact with database objects more naturally.

Now the ORM is kind of like a robot, it understands that pizza is an object, instead of raw ingredients and shelf numbers.

Now our code knows where to go, how to safely get the pizza and not destroy the pantry.

from sqlalchemy import create_engine, Table, MetaData, select

engine = create_engine('postgresql://user:pass@localhost/dbname')
metadata = MetaData()
pizzas = Table('pizzas', metadata, autoload_with=engine)

def get_pizzas(pid):
    stmt = select(pizzas).where(pizzas.c.id == pid)
    with engine.connect() as conn:
        result = conn.execute(stmt)
        return result.fetchall()

ORM Offer some nice components for our design.

  • Abstraction: You work with Python objects instead of raw SQL strings.

    • In our first iteration of code, we said 'Go to shelf 4, find Pizza' now we are saying bring the pizza object with the id of 7

  • Security: Parameter binding is handled automatically.

    • In our first code, anyone could destroy the pantry, now the ORM robot is checking the orders. It's harder to manipulate, however a dedicated person who is technically inclined still can.

  • Refactor-friendly: Changes to table structure are easier to manage.

    • If you reorganize the pantry (change table schema), the robot can adapt. We no longer have to rewrite every query.

  • Domain alignment: Queries reflect business logic more clearly.

    • Previously we had to think in a structured query language, now we are programming “I need pizza #7 for a customer order.” The instructions read like the business thinks about pizzas.

Your code is a component and just in this article we have explored different ideas next we will design a Service layer that will help us write code that we can couple and un-couple as we understand more about it, the domains and how it all comes together.

Introducing a Service Layer (Repository)

I want to draw your attention to the word Repository for a moment. As a Noun, a repository means a facility where things can be deposited for storage or safekeeping. In this case our facility is the database, inside our web-server. The database is a facility, inside of a web server. That's the 'Repository' we're thinking about here.

We're storing our logic in that repository so we can make it work with our software and make a REST API for applications that will use our pizza software.

The pizza pantry is the repository of the business kitchen, you don't make the pizza in the pantry it is where you store, organize, and retrieve them.

This abstraction separates business logic from data access and makes your system easier to maintain and test.

class PizzaRepository:
    def __init__(self, db_connection):
        self.conn = db_connection

    def get_by_id(self, pid):
        cursor = self.conn.cursor()
        cursor.execute("SELECT * FROM pizzas WHERE id = %s;", (pid,))
        return cursor.fetchall()

# Usage
repo = PizzaRepository(database)
pizza = repo.get_by_id(5)

With this pattern:

The PizzaRepository class wraps database queries so the rest of your code doesn’t directly touch the database.

  • Your data access logic is encapsulated in a single place

Now when you hire more chefs, the chefs have a pantry manager (repository) who knows exactly how to get what is needed.

No chef has to memorize where the mozzarella or toppings are stored.

  • Changes to the database or queries don’t ripple through your business logic

Chefs don’t get confused when the pantry layout changes, and tests (practice runs) can be done without the full kitchen

  • Testing becomes simpler since you can mock the repository instead of the database. You can pretend the pantry exists with fake pizzas during practice sessions. No need to disturb the real pantry.

    A note on service design:

    • Before splitting your data across multiple services, measure how often different resources are queried together. Breaking them too soon can create problems.

The design of services should be driven by both the data they host and the domain they operate in:

  • Tightly coupled data – Measure how often different resources are queried together before refactoring.

  • This informs whether you can break up your services without breaking business rules.

Now that we have covered this logic, next I want to explore REST.
REST is a principle of architecture in your design, and related to the development of your business software as an application

REST Principles and Stateless Communication

If other systems — a mobile app, a third-party delivery platform, a front-end — need to talk to your pizza back-end, consider a REST API.

REST (Representational State Transfer) is an architecture for communicating with servers. Its key principle is statelessness: the server doesn’t remember previous requests.

Imagine a pizza shop where every customer order is complete on its own slip.

Fetching a pizza by ID looks like this:

A customer goes to your website and orders pizza 5

GET /api/pizzas/5

The chef (server) doesn’t remember what Customer 5 ordered when preparing Customer 7’s pizza.

No sticky notes left from previous orders. Every pizza order is self-contained. If the chef misreads an order, you just look at that single slip.

This keeps your system easier to scale and much easier to debug if a request fails, you can inspect it in isolation without reconstructing a chain of prior interactions.

What does that mean for your business?

More chefs (servers) can jump in at any time, because they don’t need to know the history of orders.

  • Stateless communication: Each request contains all the information needed; the server does not rely on previous requests.

  • This improves scalability and simplifies debugging.

So lets think about our function and how it all comes together.

Version

Pizza Biz

What It Does Well

What It Gets Wrong

f-string SQL

Chef grabs pizza from pantry directly

Simple, readable

Dangerous if customer gives bad instructions

Parameterized query

Chef grabs pizza safely with a secure order slip

Injection-safe

Chef still tied to one pantry layout

ORM

Smart robot chef fetches pizzas as objects

Safe + abstracted

Logic still mixed with fetching

Repository

Pantry manager handles pizza retrieval

Safe, abstracted, testable

Requires upfront setup

REST API

Customer gives full order slip to server/chef

Stateless, scalable, easy to debug

Needs careful API design