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:
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)
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.
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.
Domain alignment: Queries reflect business logic more clearly.
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.
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.
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:
The design of services should be driven by both the data they host and the domain they operate in:
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.
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 |