Building a Modern Full-Stack Application: Architecture First

Published: (February 17, 2026 at 12:40 AM EST)
10 min read
Source: Dev.to

Source: Dev.to

The Journey Begins

Ever started a project that seemed simple at first, only to watch it spiral into a tangled mess of spaghetti code? I’ve been there. But what if I told you that spending time upfront on architecture could save you hundreds of hours of refactoring later?

In this series, I’m going to walk you through building a production‑ready full‑stack application with clean‑architecture principles. This isn’t theoretical fluff—this is real code, real patterns, and real lessons learned from building an enterprise‑level tutoring management system.


What We’ll Build

  • Backend – .NET 9 Web API
  • Frontend – React 19 + TypeScript
  • Database – PostgreSQL with Entity Framework Core
  • Auth – Azure AD authentication
  • Extras – Real‑time features & background processing

But more importantly, we’ll build it the right way—with clean architecture, proper layering, and maintainable patterns that scale.


Why Architecture Matters

In this first post we’ll understand WHY architecture matters by looking at what happens when you skip it. We’ll see the real problems that emerge and why “we’ll fix it later” never works.

Typical Timeline When You Skip Architecture

TimeWhat HappensConsequences
Week 1“Let’s just get something working”• Direct DB calls in controllers
• No thought to testing
• “We’ll clean it up later”
Month 3“We need to add features fast”• Copy‑paste existing code
• Each developer does things differently
• Technical debt accumulating
Month 6“Why is everything breaking?”• Changes in one place break unrelated features
• Can’t add tests (too tightly coupled)
• Fear of touching existing code
Month 12“We need to rewrite this”• Too expensive to fix
• Business pressure to keep adding features
• Team morale drops

The Truth: Later never comes. Technical debt compounds like credit‑card interest. What takes 1 hour to do right initially can take 10 hours to fix later—or 100 hours to rewrite.


Two Approaches

Code‑First (Bad)

Start coding → Hit problems → Try to refactor → Too late → Live with mess
  • ✅ Fast initial progress
  • ❌ Slows down dramatically over time
  • ❌ Hard to test
  • ❌ Difficult to maintain
  • ❌ Expensive to change

Architecture‑First (Good)

Plan architecture → Implement with patterns → Maintain structure → Scale easily
  • ⚠️ Slower initial start (1‑2 days planning)
  • ✅ Consistent velocity over time
  • ✅ Easy to test
  • ✅ Simple to maintain
  • ✅ Changes are isolated and safe

Real‑world analogy: Building a house. You can start nailing boards together and see progress immediately, but without a blueprint you’ll end up with crooked walls and no plumbing. Architects spend weeks on blueprints because it saves months during construction.


Example: Skipping Architecture

Below is real code that “works” but creates a host of problems.

// Controller.cs – What happens without architecture
public class StudentController : ControllerBase
{
    [HttpGet]
    public async Task GetStudents()
    {
        // Direct database access in controller? Bad!
        using var connection = new NpgsqlConnection("connection_string");
        var students = await connection.QueryAsync("SELECT * FROM Students");

        // Business logic in controller? Also bad!
        foreach (var student in students)
        {
            if (student.Age ("SELECT * FROM Students");` | Raw SQL query in controller | • Controller knows table/column details 
• Any schema change forces controller changes 
• `SELECT *` fetches unnecessary data 
• Opens the door to SQL‑injection if parameters are added |
| **37** – Business logic & entity exposure | Logic and data models live in the controller | • Violates separation of concerns 
• Makes reuse difficult 
• Exposes internal DB schema to API consumers |

> **Analogy:** It’s like a restaurant waiter walking into the kitchen, cooking the food themselves, and serving it. The waiter should only take orders and deliver food—not operate the stove.

---

## Breaking Down the Raw SQL Query  

```csharp
var students = await connection.QueryAsync("SELECT * FROM Students");
  • Raw SQL query – Direct command written in Structured Query Language (SQL), the language databases understand. “Raw” means you’re writing the exact SQL yourself instead of using a higher‑level abstraction like EF Core.
  • Maps results to Student objects – The database returns rows (think of a spreadsheet). QueryAsync converts each row into a C# Student instance.

Example result set

IdNameAgeEmail
1John Doe20j@s.com
2Jane Doe19ja@s.com

Mapped to C# objects

new Student { Id = 1, Name = "John Doe", Age = 20, Email = "j@s.com" };
new Student { Id = 2, Name = "Jane Doe", Age = 19, Email = "ja@s.com" };

Why This Is Bad

  • Tight coupling – Change the table name or column list → you must edit the controller.
  • PerformanceSELECT * pulls every column even if you only need a few.
  • Security – Adding user‑supplied parameters without proper handling leads to SQL injection.

What Is SQL Injection?

SQL injection occurs when an attacker tricks your application into executing malicious SQL commands. It’s one of the most dangerous security vulnerabilities.

Vulnerable example (DO NOT USE):

// NEVER DO THIS! ☠️ Extremely dangerous!
public async Task GetStudentByName(string name)
{
    // Building query by concatenating user input
    var query = $"SELECT * FROM Students WHERE Name = '{name}'";
    var students = await connection.QueryAsync(query);
    return Ok(students);
}

Next Steps

In the upcoming posts we’ll refactor the above into a clean‑architecture solution:

  1. Domain layer – Pure business rules (Student entity, use‑cases).
  2. Application layer – Interfaces & DTOs.
  3. Infrastructure layer – EF Core repository, PostgreSQL connection.
  4. Presentation layer – Controllers that orchestrate use‑cases only.

Stay tuned—next time we’ll start with domain modeling and show how a tiny amount of upfront planning saves countless hours later. 🚀

var query = "SELECT * FROM Students WHERE Name = '" + name + "'";
var student = await connection.QueryAsync(query);
return Ok(student);

What happens when a normal user searches for “John”?

  • Query becomes:
    SELECT * FROM Students WHERE Name = 'John'
  • ✅ Works fine, returns John’s record.

What happens when an ATTACKER enters:

John'; DROP TABLE Students; --
  • Query becomes:
    SELECT * FROM Students WHERE Name = 'John'; DROP TABLE Students; --'
  • ☠️ DISASTER!
    1. Selects John.
    2. Deletes the entire Students table.
    3. -- comments out the rest of the statement.

Your entire Students table is gone – all student data deleted forever.


Even worse attacks

Attack inputResulting queryEffect
' OR '1'='1SELECT * FROM Students WHERE Name = '' OR '1'='1'☠️ Returns all students (data exposure).
'; UPDATE Students SET GPA = 4.0 WHERE Name = 'Attacker'; --SELECT * FROM Students WHERE Name = ''; UPDATE Students SET GPA = 4.0 WHERE Name = 'Attacker'; --'☠️ Changes grades (data manipulation).
'; SELECT password, email FROM Users; --(adds a second statement)☠️ Steals credentials.

Why this happens

  • User input is treated as code instead of data.
  • The database cannot distinguish between the intended query and the attacker’s injected commands.
  • It’s like giving someone a form to fill out and they write instructions in the blank spaces that you then follow blindly.

The Safe Way – Parameterized Queries

public async Task GetStudentByName(string name)
{
    var query = "SELECT * FROM Students WHERE Name = @Name";
    var student = await connection.QueryAsync(query, new { Name = name });
    return Ok(student);
}

What changes?

  • @Name is a parameter placeholder.
  • The value is passed separately: new { Name = name }.
  • The database driver automatically escapes the input.
  • User input is treated as data, never as code.

Example with the same malicious input

  • Input: John'; DROP TABLE Students; --
  • Query stays: SELECT * FROM Students WHERE Name = @Name
  • Parameter value: @Name = "John'; DROP TABLE Students; --"
  • The database treats the entire string as a name to search for, finds nothing, and returns an empty result.
  • Your table is safe!

Real‑world impact

  • 2008 – Heartland Payment Systems: 134 million credit cards stolen via SQL injection.
  • 2012 – Yahoo: 450 000 passwords leaked.
  • 2017 – Equifax breach: 147 million people affected (SQL injection later found in their systems).

SQL injection has been in the OWASP Top 10 security risks for over a decade.

Lesson: NEVER concatenate user input into SQL queries. Always use parameterized queries or an ORM (e.g., Entity Framework) that handles this automatically.


Other Bad Practices in the Sample Code

1️⃣ Business Logic in the Controller

foreach (var student in students)
{
    if (student.Age  **Architecture is not overhead. Architecture is debt prevention.**

---

## What’s Next?

In **Part 2** well explore the different architectural approaches available:

1. **No Architecture (Script Pattern)** – When is it okay?  
2. **Traditional N‑Tier** – Better, but still has problems.  
3. **Active Record Pattern** – Simple but limiting.  
4. **Repository Pattern** – Getting closer.  
5. **Domain‑Driven Design** – For complex domains.

## Microservices – The Scaling Solution  
### Clean Architecture – The Sweet Spot ⭐  

For each pattern, I’ll show you:

- Real code examples with line‑by‑line explanations  
- What problems it solves  
- What problems it creates  
- When to use it (and when **NOT** to)  
- Why Clean Architecture wins for most production applications  

---

### Why Architecture Matters  

-**“We’ll fix it later” never happens** – Technical debt compounds exponentially.  
-**Without architecture, every change becomes dangerous** – Fear of touching code kills velocity.  
-**SQL injection is real and devastating** – Billions of dollars lost due to this vulnerability.  
- ✅ **Architecture is debt prevention, not overhead**10 hours invested saves hundreds later.  
-**Controllers doing everything is a time bomb** – Database, business logic, HTTP all mixed together.  
-**Testing becomes impossible without separation** – Can’t test what you can’t isolate.  
-**Team scalability requires structure** – New developers need clear boundaries.  

---

### Discussion Prompt  

Have you experienced the pain of “we’ll fix it later”?  
What was the tipping point that made you invest in architecture?  
Share your stories in the comments below!

---

### Next in the Series  

**Part 2:** Comparing Architectural Approaches – Finding the Right Pattern  

---

### Tags  

`#dotnet` `#csharp` `#architecture` `#softwaredevelopment` `#webapi` `#programming` `#technicaldebt` `#coding`

---

*This series is based on real experiences building an enterprise tutoringmanagement system. All code examples have been generalized for educational purposes.*
0 views
Back to Blog

Related posts

Read more »