Software Development Basics

Learn Software Architecture, SOLID Principles, Python & Go Programming

Software Architecture Fundamentals

Layered Architecture

Organizes code into horizontal layers with specific responsibilities

Presentation Layer
Business Logic Layer
Data Access Layer
Database Layer

Microservices Architecture

Breaks applications into small, independent services

User Service
Order Service
Payment Service
Notification Service

MVC Pattern

Separates application logic into Model, View, and Controller

Model
Data & Logic
View
User Interface
Controller
Input Handler

Architecture Implementation Examples

Layered Architecture in Python

# Presentation Layer
class UserController:
    def __init__(self, user_service):
        self.user_service = user_service

    def create_user(self, user_data):
        try:
            user = self.user_service.create_user(user_data)
            return {"status": "success", "user": user}
        except Exception as e:
            return {"status": "error", "message": str(e)}

# Business Logic Layer
class UserService:
    def __init__(self, user_repository):
        self.user_repository = user_repository

    def create_user(self, user_data):
        if not self._validate_user_data(user_data):
            raise ValueError("Invalid user data")

        # Business logic
        user_data['email'] = user_data['email'].lower()
        return self.user_repository.save(user_data)

    def _validate_user_data(self, data):
        return 'email' in data and '@' in data['email']

# Data Access Layer
class UserRepository:
    def __init__(self, database):
        self.database = database

    def save(self, user_data):
        query = "INSERT INTO users (email, name) VALUES (?, ?)"
        return self.database.execute(query, user_data['email'], user_data['name'])

    def find_by_email(self, email):
        query = "SELECT * FROM users WHERE email = ?"
        return self.database.execute(query, email)

Layered Architecture in Go

package main

import (
    "errors"
    "strings"
)

// Domain Layer
type User struct {
    ID    int    `json:"id"`
    Email string `json:"email"`
    Name  string `json:"name"`
}

// Data Access Layer
type UserRepository interface {
    Save(user User) (*User, error)
    FindByEmail(email string) (*User, error)
}

type userRepository struct {
    db Database
}

func (r *userRepository) Save(user User) (*User, error) {
    query := "INSERT INTO users (email, name) VALUES ($1, $2) RETURNING id"
    err := r.db.QueryRow(query, user.Email, user.Name).Scan(&user.ID)
    return &user, err
}

// Business Logic Layer
type UserService struct {
    repo UserRepository
}

func (s *UserService) CreateUser(userData User) (*User, error) {
    if err := s.validateUserData(userData); err != nil {
        return nil, err
    }

    // Business logic
    userData.Email = strings.ToLower(userData.Email)
    return s.repo.Save(userData)
}

func (s *UserService) validateUserData(data User) error {
    if data.Email == "" || !strings.Contains(data.Email, "@") {
        return errors.New("invalid email")
    }
    return nil
}

// Presentation Layer
type UserController struct {
    service *UserService
}

func (c *UserController) CreateUser(userData User) map[string]interface{} {
    user, err := c.service.CreateUser(userData)
    if err != nil {
        return map[string]interface{}{
            "status": "error",
            "message": err.Error(),
        }
    }

    return map[string]interface{}{
        "status": "success",
        "user":   user,
    }
}

SOLID Principles

S - Single Responsibility Principle

A class should have only one reason to change

❌ User class handling data + email + logging
✅ User + EmailService + Logger

O - Open/Closed Principle

Open for extension, closed for modification

❌ Modifying existing code for new features
✅ Extending through interfaces/inheritance

L - Liskov Substitution Principle

Objects should be replaceable with instances of their subtypes

❌ Square breaking Rectangle behavior
✅ Proper inheritance hierarchy

I - Interface Segregation Principle

Clients shouldn't depend on interfaces they don't use

❌ Fat interfaces with unused methods
✅ Small, focused interfaces

D - Dependency Inversion Principle

Depend on abstractions, not concretions

❌ Direct dependency on concrete classes
✅ Dependency on interfaces

Single Responsibility Principle Examples

❌ Violating SRP

class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

    def save_to_database(self):
        # Database logic
        pass

    def send_email(self):
        # Email logic
        pass

    def log_activity(self):
        # Logging logic
        pass

✅ Following SRP

class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

class UserRepository:
    def save(self, user):
        # Database logic
        pass

class EmailService:
    def send_email(self, user, message):
        # Email logic
        pass

class Logger:
    def log_activity(self, user, activity):
        # Logging logic
        pass

❌ Violating SRP

type User struct {
    Name  string
    Email string
}

func (u *User) SaveToDatabase() error {
    // Database logic
    return nil
}

func (u *User) SendEmail() error {
    // Email logic
    return nil
}

func (u *User) LogActivity() error {
    // Logging logic
    return nil
}

✅ Following SRP

type User struct {
    Name  string
    Email string
}

type UserRepository struct{}

func (r *UserRepository) Save(user *User) error {
    // Database logic
    return nil
}

type EmailService struct{}

func (e *EmailService) SendEmail(user *User, message string) error {
    // Email logic
    return nil
}

type Logger struct{}

func (l *Logger) LogActivity(user *User, activity string) error {
    // Logging logic
    return nil
}

Design Patterns

Singleton Pattern

Ensures only one instance of a class exists

🏛️ Single Instance

Factory Pattern

Creates objects without specifying exact classes

🏭 Object Factory

Observer Pattern

Notifies multiple objects about state changes

📢 Publisher → Subscribers

Strategy Pattern

Defines family of algorithms and makes them interchangeable

🔄 Interchangeable Algorithms

Singleton Pattern Implementation

class DatabaseConnection:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls):
        if cls._instance is None:
            with cls._lock:
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
                    cls._instance._initialize()
        return cls._instance

    def _initialize(self):
        self.connection = "Database connection established"

    def query(self, sql):
        return f"Executing: {sql}"

# Usage
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2)  # True - same instance
package main

import (
    "sync"
)

type DatabaseConnection struct {
    connection string
}

var (
    instance *DatabaseConnection
    once     sync.Once
)

func GetDatabaseConnection() *DatabaseConnection {
    once.Do(func() {
        instance = &DatabaseConnection{
            connection: "Database connection established",
        }
    })
    return instance
}

func (db *DatabaseConnection) Query(sql string) string {
    return fmt.Sprintf("Executing: %s", sql)
}

// Usage
func main() {
    db1 := GetDatabaseConnection()
    db2 := GetDatabaseConnection()
    fmt.Println(db1 == db2) // true - same instance
}

Python vs Go Comparison

Language Characteristics

Feature Python Go
Type System Dynamic Static
Compilation Interpreted Compiled
Concurrency Threading/Asyncio Goroutines
Memory Management Garbage Collected Garbage Collected
Performance Slower Faster

Use Cases

Python Best For:

  • Data Science & ML
  • Web Development (Django/Flask)
  • Scripting & Automation
  • Rapid Prototyping
  • Scientific Computing

Go Best For:

  • Microservices
  • System Programming
  • Cloud Infrastructure
  • Network Services
  • High-Performance APIs

Side-by-Side Code Examples

HTTP Server Implementation

Python (Flask)
from flask import Flask, jsonify
import threading
import time

app = Flask(__name__)

# Simulated database
users = []
user_id_counter = 1

@app.route('/users', methods=['GET'])
def get_users():
    return jsonify(users)

@app.route('/users', methods=['POST'])
def create_user():
    global user_id_counter
    user = {
        'id': user_id_counter,
        'name': request.json.get('name'),
        'email': request.json.get('email'),
        'created_at': time.time()
    }
    users.append(user)
    user_id_counter += 1
    return jsonify(user), 201

@app.route('/users/', methods=['GET'])
def get_user(user_id):
    user = next((u for u in users if u['id'] == user_id), None)
    if user:
        return jsonify(user)
    return jsonify({'error': 'User not found'}), 404

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080, threaded=True)
Go (net/http)
package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "strconv"
    "sync"
    "time"
    "github.com/gorilla/mux"
)

type User struct {
    ID        int       `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"created_at"`
}

var (
    users         []User
    userIDCounter int = 1
    usersMutex    sync.RWMutex
)

func getUsers(w http.ResponseWriter, r *http.Request) {
    usersMutex.RLock()
    defer usersMutex.RUnlock()

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(users)
}

func createUser(w http.ResponseWriter, r *http.Request) {
    var user User
    json.NewDecoder(r.Body).Decode(&user)

    usersMutex.Lock()
    user.ID = userIDCounter
    user.CreatedAt = time.Now()
    users = append(users, user)
    userIDCounter++
    usersMutex.Unlock()

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

func getUser(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    userID, _ := strconv.Atoi(vars["id"])

    usersMutex.RLock()
    defer usersMutex.RUnlock()

    for _, user := range users {
        if user.ID == userID {
            w.Header().Set("Content-Type", "application/json")
            json.NewEncoder(w).Encode(user)
            return
        }
    }

    w.WriteHeader(http.StatusNotFound)
    json.NewEncoder(w).Encode(map[string]string{"error": "User not found"})
}

func main() {
    r := mux.NewRouter()
    r.HandleFunc("/users", getUsers).Methods("GET")
    r.HandleFunc("/users", createUser).Methods("POST")
    r.HandleFunc("/users/{id}", getUser).Methods("GET")

    fmt.Println("Server starting on :8080")
    http.ListenAndServe(":8080", r)
}