BYU Strategy - Marriott School of Business

SWE Principles

Types of Digital Applications

1. System Software

Operating systems, device drivers, and utilities that manage hardware and core system functions (e.g., Windows, Linux, macOS).

2. Embedded Software

Runs on specialized hardware such as IoT devices, vehicles, and appliances (e.g., firmware in smart thermostats or car infotainment systems).

3. APIs and Microservices (Supporting Components)

Backend services and interfaces that enable communication and integration between applications (e.g., Stripe API, AWS Lambda).

4. Cloud-Based Applications / SaaS

Applications accessed over the internet, often subscription-based and scalable (e.g., Salesforce, Zoom, Dropbox).

5. Web Applications

Apps accessed via a browser, including: - Standard web apps – Gmail, Google Docs, Netflix. - Progressive Web Apps (PWAs) – Hybrid apps with offline support and installability (e.g., Twitter Lite).

6. Desktop Applications

Installed software for personal computers (e.g., Photoshop, Microsoft Word, Spotify desktop).

7. Mobile Applications

Native or cross-platform apps for smartphones and tablets (e.g., Instagram, Uber, Duolingo).

Software Engineering Fundamentals & Design Principles

Introduction: Thinking like a Software Engineer

What comes to mind when you think of software engineering? Most people immediately think of one thing–writing code. However, “writing code” does not fully encapsulate the work of a software engineer. At its core, software engineering is all about designing solutions to complex problems in a way that others can understand and improve upon over time. Whether you’re building a personal project or contributing to a larger application, you should always keep these four essential questions in mind:

  • Can others understand my code? Will someone else (or future you) be able to read and modify this code easily?
  • Is it scalable? As my user base grows, will my code still work efficiently?
  • Does it handle errors? When something goes wrong, does my code deal with it gracefully instead of crashing?
  • Does it work fast? Is my code efficient and doesn’t waste computer resources?

Understanding the fundamentals of software engineering is essential to allow you to use AI to its fullest capacity. This chapter explores those fundamental ideas and best practices needed to write robust and maintainable code.

Programming Basics

Variables, Functions, Conditionals, Loops

The building blocks of any program are variables, functions, conditionals, and loops. Below is a brief introduction to each programming concept, along with some practical examples:

** About the Code Examples**

In the code examples throughout this chapter, we will use JavaScript with Next.js, a popular React framework for building modern web applications. Keep in mind though, any programming language will adhere to the same fundamental principles, though the syntax may vary.

Variables store data in your program. Choose descriptive names that clearly indicate what the variable contains:

// Good - Names clearly describe what the data represents
const userAge = 25;                    // Age of the user
const isAuthenticated = true;          // Whether user is logged in
const customerEmail = "john@example.com"; // User's email address

// Poor - Unclear names that don't explain the data
const x = 25;        // What does x represent?
const flag = true;   // What is this flag for?
const data = "john@example.com"; // What kind of data?

Functions are reusable blocks of code that perform specific tasks. Good functions follow these principles:

// This function has one clear job: calculate a price with tax and discount
// It takes three inputs (parameters) and returns the final price
function calculateTotalPrice(basePrice, taxRate, discountPercentage = 0) {
    // First, apply the discount (if any)
    const discountedPrice = basePrice * (1 - discountPercentage / 100);
    
    // Then, add tax to the discounted price
    const totalWithTax = discountedPrice * (1 + taxRate);
    
    // Round to 2 decimal places for currency display
    return Math.round(totalWithTax * 100) / 100;
}

// Usage example:
const finalPrice = calculateTotalPrice(100, 0.08, 10); // $100 base, 8% tax, 10% discount
console.log(finalPrice); // Result: $97.20

Conditionals let your program make decisions based on different situations:

// Check user's age and respond appropriately
if (userAge >= 18) {
    console.log("User is an adult");
} else if (userAge >= 13) {
    console.log("User is a teenager");
} else {
    console.log("User is a child");
}

// This creates different behavior based on the userAge value
// 25 → "User is an adult"
// 16 → "User is a teenager" 
// 8 → "User is a child"

Loops repeat actions efficiently instead of writing the same code multiple times:

// For loop: when you know exactly how many times to repeat
for (let i = 0; i < 5; i++) {
    console.log(`Iteration ${i}`); // Prints: Iteration 0, Iteration 1, etc.
}

// While loop: when you repeat until a condition is met
let userInput = "";
while (userInput !== "quit") {
    userInput = prompt("Enter command (or 'quit' to exit): ");
    processCommand(userInput);
}
// This keeps asking for input until the user types "quit"

Components in Next.js

In Next.js, components are like custom LEGO blocks for building websites. Each component is a piece of your website that can be reused and combined with other pieces.

Think of a component like a recipe that describes: - What it looks like (the display) - What data it needs (like ingredients) - What it can do (like buttons that respond to clicks)

Let’s look at a simple example - a counter component that can be used on any page:

// components/Counter.js
import { useState } from 'react';

// This is a Counter component - it's like creating a new type of element
export default function Counter() {
    // This is "state" - data that can change over time
    // We start counting at 0
    const [count, setCount] = useState(0);
    
    // These are functions that change our count
    const increase = () => {
        setCount(count + 1); // Add 1 to current count
    };
    
    const decrease = () => {
        setCount(count - 1); // Subtract 1 from current count
    };
    
    // This describes what appears on the screen
    return (
        <div>
            <h2>Counter: {count}</h2>
            <button onClick={increase}>+</button>
            <button onClick={decrease}>-</button>
        </div>
    );
}

Using the component in a Next.js App Router page:

// app/page.js (Home page)
import Counter from '../components/Counter';

export default function HomePage() {
    return (
        <div>
            <h1>Welcome to My App</h1>
            <Counter />
        </div>
    );
}

What’s happening here: 1. useState(0) creates a variable called count that starts at 0 2. setCount is how we change the value of count 3. When someone clicks the + button, count increases by 1 4. When someone clicks the - button, count decreases by 1 5. The screen automatically updates to show the new number 6. export default makes the component available to import in other files

Making components reusable with props:

// components/UserCard.js
export default function UserCard({ name, age, email }) {
    return (
        <div style={{ border: '1px solid #ccc', padding: '16px', margin: '8px' }}>
            <h3>{name}</h3>
            <p>Age: {age}</p>
            <p>Email: {email}</p>
        </div>
    );
}

// Usage in a page:
// <UserCard name="Alice" age={25} email="alice@example.com" />
// <UserCard name="Bob" age={30} email="bob@example.com" />

Next.js App Router benefits: - File-based routing: Folders become routes, page.js files become pages - Layouts and templates: Shared UI components that wrap multiple pages - Built-in optimization: Images, fonts, and scripts are optimized automatically - Server and client components: Better performance with server-side rendering - API routes: Backend and frontend in the same project - Easy deployment: One command deploys your entire app

Command Line Fundamentals

The command line is your direct interface to the computer’s operating system. It’s faster than clicking through menus once you learn the basics.

Essential Commands

cd (Change Directory) - Navigate between folders:

cd /path/to/your/project    # Go to a specific folder
cd ..                       # Go up one level (to parent folder)
cd ~                        # Go to your home directory
cd -                        # Go back to the previous directory

ls (List) - See what’s in the current folder:

ls                          # Show files and folders
ls -la                      # Show detailed info including hidden files
ls *.py                     # Show only Python files

mkdir (Make Directory) - Create new folders:

mkdir new-project           # Create a single folder
mkdir -p projects/web/app   # Create nested folders all at once

Additional Useful Commands:

pwd                         # Show current folder path
rm filename                 # Delete a file
rm -rf foldername          # Delete a folder and everything in it
cp source destination       # Copy files
mv source destination       # Move or rename files

Command Line Tips

  • Tab completion: Start typing a filename and press Tab to auto-complete
  • Up arrow: Recalls your previous commands
  • Ctrl+C: Stops a running command
  • man command: Shows the manual for any command (e.g., man ls)

Project Setup and Structure

How to Create a New Project

Starting with good organization prevents headaches later. Here’s the standard workflow:

1. Create the project folder:

mkdir my-awesome-project    # Create main folder
cd my-awesome-project       # Enter the folder

2. Set up version control:

git init                    # Initialize Git tracking

3. Create basic structure:

mkdir src                   # Source code goes here
mkdir tests                 # Test files go here
mkdir docs                  # Documentation goes here
touch README.md             # Project description file
touch .gitignore            # Files to ignore in Git

4. Set up for Next.js:

# Create a new Next.js project (recommended)
npx create-next-app@latest my-awesome-project
cd my-awesome-project

# Or manual setup (advanced users)
npm init -y
npm install next react react-dom

File Structure (One Class/Component per File)

Organize your code so each file has one main purpose. This makes finding and fixing code much easier. Below is an example of how you might organize your codebase:

my-nextjs-project/
├── app/                   # App Router (Next.js 13+)
│   ├── layout.js         # Root layout component
│   ├── page.js           # Home page (/)
│   ├── globals.css       # Global styles
│   ├── login/            # Login page route
│   │   └── page.js       # Login page (/login)
│   ├── products/         # Products route group
│   │   ├── page.js       # Products list (/products)
│   │   └── [id]/         # Dynamic route
│   │       └── page.js   # Individual product (/products/123)
│   └── api/              # API routes (backend)
│       ├── users/        # /api/users endpoint
│       │   └── route.js  # API handler
│       └── orders/       # /api/orders endpoint
│           └── route.js  # API handler
├── components/           # Reusable UI components
│   ├── User.js          # User component
│   ├── Product.js       # Product component
│   └── Order.js         # Order component
├── lib/                 # Business logic and utilities
│   ├── userService.js   # User-related operations
│   ├── emailService.js  # Email functionality
│   ├── paymentService.js # Payment processing
│   └── utils.js         # General helper functions
├── styles/              # Additional CSS files
│   └── components.css   # Component-specific styles
├── public/              # Static files
│   ├── favicon.ico      # Website icon
│   └── images/          # Image assets
├── __tests__/           # Test files
│   ├── components/      # Component tests
│   │   └── User.test.js
│   ├── app/            # Page tests
│   │   └── page.test.js
│   └── lib/            # Service tests
│       └── userService.test.js
├── README.md            # Project documentation
├── package.json         # Dependencies and scripts
├── next.config.js       # Next.js configuration
└── .gitignore          # Files to ignore in version control

Why this file structure works: - Clear separation: Components, logic, and styles are organized separately - Easy navigation: Need user functionality? Look in User.js or userService.js - Prevents conflicts: Team members can work on different files without interfering - Easier testing: Test files directly correspond to source files

Running Your Project Locally

Localhost

Localhost is your computer pretending to be a web server. It lets you test your website before publishing it to the internet.

Starting your local Next.js server:

# For Next.js projects:
npm run dev                 # Starts Next.js development server on port 3000

# For production build testing:
npm run build               # Build the app for production
npm start                   # Start production server

# For static export:
npm run build && npm run export  # Generate static files

What happens when you run npm run dev: 1. The Next.js development server starts 2. Your browser opens to http://localhost:3000 3. You can see your app running locally 4. Changes you make automatically refresh the page (Hot Reload) 5. API routes are available at http://localhost:3000/api/...

Local Development Tips: - Use environment variables for settings (database URLs, API keys) - Keep local and production environments similar - Use different ports for different services to avoid conflicts

Version Control with Git

Git tracks changes to your code over time, like a detailed history of your project. It lets you experiment safely and collaborate with others. Learning to use version control is a crucial part of managing any software project. See the following video for a brief introduction to git:

https://www.youtube.com/watch?v=r8jQ9hVA2qs

See the Github and Collaboration chapter for more detailed information on using Git and Github.

Debugging and Error Handling

How to Handle Errors

Good programs expect things to go wrong and handle problems gracefully instead of crashing.

Try-catch blocks handle errors that might occur:

// This function tries to do something risky and handles problems
function processUserData(userData) {
    try {
        // This might fail if userData is invalid
        const result = parseUserInput(userData);
        const processedData = transformData(result);
        return processedData;
        
    } catch (error) {
        // Handle specific types of errors differently
        if (error instanceof ValidationError) {
            console.error(`User input invalid: ${error.message}`);
            return null; // Return safe default
        } else {
            console.error(`Unexpected error: ${error.message}`);
            throw error; // Re-throw unknown errors
        }
        
    } finally {
        // This code runs whether success or failure
        cleanupTempFiles();
    }
}

Input validation checks data before using it:

function processUserAge(ageInput) {
    // Convert input to number
    const age = parseInt(ageInput);
    
    // Check if it's a valid age
    if (isNaN(age)) {
        throw new Error("Age must be a number");
    }
    if (age < 0 || age > 150) {
        throw new Error("Age must be between 0 and 150");
    }
    
    return age;
}

// Usage with error handling:
try {
    const userAge = processUserAge("25");  // Valid
    console.log(`User is ${userAge} years old`);
} catch (error) {
    console.error(`Invalid age: ${error.message}`);
}

Graceful degradation provides backups when things fail:

async function getUserProfilePicture(userId) {
    try {
        // Try to get image from fast CDN first
        return await fetchFromCDN(userId);
    } catch (cdnError) {
        try {
            // CDN failed, try backup storage
            return await fetchFromBackupStorage(userId);
        } catch (backupError) {
            // Both failed, return default image
            return getDefaultAvatar();
        }
    }
}

Setting Up Your Debugger

Debuggers let you pause your program and inspect what’s happening step by step.

VS Code setup: 1. Press Ctrl+Shift+D (Run and Debug view) 2. Click “create a launch.json file” 3. Choose your language (JavaScript, Python, etc.) 4. VS Code creates a configuration file for debugging

Browser Developer Tools: - Press F12 to open - Console tab: See error messages and run JavaScript commands - Sources tab: Set breakpoints and step through code - Network tab: See API calls and responses

Setting Breakpoints

Breakpoints pause your program so you can examine the current state.

In your code (JavaScript):

function calculateTotal(items) {
    let total = 0;
    
    debugger; // Program pauses here when debugger is open
    
    for (let item of items) {
        total += item.price;
        console.log(`Added ${item.name}: $${item.price}, total: $${total}`);
    }
    
    return total;
}

In your IDE: - Click in the left margin next to line numbers - Red dot = active breakpoint (program will pause here) - Gray dot = disabled breakpoint

Inspecting Your Changes

When your program is paused at a breakpoint, you can:

  • View variables: See current values of all variables in scope
  • Step through code: Execute one line at a time to see what happens
  • Call stack: See the chain of function calls that led to this point
  • Console commands: Type commands to test things in the current context

Debugging workflow: 1. Reproduce the bug: Make it happen consistently 2. Add breakpoints: At the start of the problematic function 3. Run with debugger: Step through line by line 4. Check your assumptions: Are variables what you expect? 5. Find the problem: Where does the code do something unexpected? 6. Fix and test: Make the change and verify it works

Design Principles

DRY (Don’t Repeat Yourself)

DRY means each piece of knowledge in your system should exist in exactly one place. When you copy and paste code, you create maintenance problems.

Bad example (lots of repetition):

// Three similar functions with duplicated logic
function calculateStudentPrice(basePrice) {
    return basePrice * 0.9; // 10% discount
}

function calculateSeniorPrice(basePrice) {
    return basePrice * 0.85; // 15% discount  
}

function calculateMilitaryPrice(basePrice) {
    return basePrice * 0.8; // 20% discount
}

Problem: If you need to change how discounts work, you have to update three places. Easy to miss one and create bugs.

Good example (DRY approach):

// One function handles all discount types
const DISCOUNT_RATES = {
    student: 0.10,
    senior: 0.15,
    military: 0.20,
    regular: 0.00
};

function calculateDiscountedPrice(basePrice, customerType) {
    const discountRate = DISCOUNT_RATES[customerType] || 0;
    return basePrice * (1 - discountRate);
}

// Usage examples:
const studentPrice = calculateDiscountedPrice(100, 'student');   // $90
const seniorPrice = calculateDiscountedPrice(100, 'senior');     // $85
const regularPrice = calculateDiscountedPrice(100, 'regular');   // $100

Benefits: One place to update discount logic, easy to add new customer types, less chance for bugs.

Commit Often and Frequently

Regular git commits create a detailed project history and make it easy to track down when bugs were introduced.

Commit frequency guidelines: - Too frequent: Every line change (clutters history) - Too infrequent: Once per week (hard to track down problems) - Just right: Complete logical units of work (features, bug fixes, improvements)

What makes a good commit: - Atomic: Contains one complete change - Working: Code compiles and tests pass - Descriptive: Message clearly explains what changed

Documentation

Good documentation saves time for everyone who works with your code, including future you.

Function documentation example:

/**
 * Calculates compound interest on an investment.
 * 
 * @param {number} principal - Starting amount of money ($)
 * @param {number} rate - Annual interest rate (as decimal: 0.05 = 5%)
 * @param {number} time - Number of years to invest
 * @param {number} [compoundsPerYear=1] - How often interest compounds (optional, defaults to 1)
 * @returns {number} Final amount after compound interest
 * 
 * @example
 * // $1000 invested for 2 years at 5% annual rate, compounded quarterly
 * const result = calculateCompoundInterest(1000, 0.05, 2, 4);
 * console.log(result); // 1103.81
 */
function calculateCompoundInterest(principal, rate, time, compoundsPerYear = 1) {
    return principal * Math.pow((1 + rate / compoundsPerYear), (compoundsPerYear * time));
}

Types of documentation: - Inline comments: Explain complex or non-obvious code - Function docs: Purpose, parameters, return values, examples - README files: Project overview, setup instructions, how to use - API docs: For interfaces other developers will use

When to document: - Complex algorithms or business logic - Functions that others will use - Unusual or non-standard approaches - Setup and configuration steps

Simple is Better than Complex

Simplicity makes code easier to understand, debug, and maintain. Always choose the simpler solution when it works just as well.

Complex approach (hard to understand):

// What does this function do? Hard to tell at a glance.
function f(x, y, z = null) {
    return !z ? x : z < 0 ? y : z > 0 ? x + y : x * y;
}

Simple approach (clear intent):

function calculatePrice(basePrice, extraFee, operation = null) {
    // No operation specified: return base price
    if (operation === null) {
        return basePrice;
    }
    
    // Negative operation: return just the extra fee
    if (operation < 0) {
        return extraFee;
    }
    
    // Positive operation: add base price and extra fee
    if (operation > 0) {
        return basePrice + extraFee;
    }
    
    // Zero operation: multiply base price by extra fee
    return basePrice * extraFee;
}

Strategies for simplicity: - Descriptive names: calculateUserAge() instead of calc() - Small functions: Each function does one clear thing - Avoid clever tricks: Code should be obvious, not impressive - Standard patterns: Use common approaches rather than inventing new ones

A Note on AI

AI tools like ChatGPT and GitHub Copilot can significantly speed up development, but they need careful oversight to maintain code quality.

AI strengths: - Generates boilerplate code quickly - Follows established patterns - Explains complex concepts
- Suggests solutions to common problems - Creates initial test cases

AI limitations and how to handle them:

1. Code duplication - AI often repeats similar code:

// AI might generate repetitive functions:
function validateEmail(email) { /* validation code */ }
function validatePassword(pass) { /* similar validation code */ }
function validateUsername(user) { /* similar validation code */ }

// You should refactor to:
function validateInput(input, type) { /* unified validation */ }

2. Missing context - AI doesn’t understand your specific codebase:

// AI suggestion might ignore your existing utilities:
function formatDate(dateString) {
    const date = new Date(dateString);
    return date.toLocaleDateString();
}

// But you already have: utils/dateHelper.js with formatDate()
// Better to use existing code for consistency

3. Security issues - Always review for vulnerabilities:

// AI might suggest unsafe patterns:
function getUserData(userId) {
    const query = `SELECT * FROM users WHERE id = ${userId}`; // SQL injection risk!
    return database.query(query);
}

// Always use parameterized queries:
function getUserData(userId) {
    const query = 'SELECT * FROM users WHERE id = ?';
    return database.query(query, [userId]);
}

Best practices with AI:

  1. Always review generated code - Treat it as a first draft, not final solution
  2. Ask follow-up questions: “Can this be simplified?” or “Is this redundant with existing code?”
  3. Test thoroughly - AI code may not handle edge cases
  4. Check for patterns - Ensure consistency with your existing codebase
  5. Validate security - Review for common vulnerabilities

Example AI workflow:

You: "Create a function to validate user registration data"
AI: [generates code]
You: "Please check if this duplicates any existing validation in my codebase"
AI: [reviews and suggests using existing validators]
You: "Add error handling and tests for edge cases"
AI: [improves the code with proper error handling]

Remember: AI is a powerful assistant that can make you more productive, but it cannot replace good engineering judgment. Use it to augment your skills, not replace your thinking.


By following these principles and practices, you’ll develop the mindset and skills necessary to write high-quality code. Remember that becoming proficient takes time and practice, so be patient with yourself as you learn and apply these concepts in your own projects.