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
Data & Logic
View
User Interface
User Interface
Controller
Input Handler
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)
}