Cooking. It’s been one of my most favorite hobbies. Oh, imagine learning to bake. At first, you follow recipes step by step. You measure 200 grams of flour and 100 grams of sugar. Then, bake at 180°C for 20 minutes. You learn to make a basic cake. But what happens when you want to make cupcakes, muffins, or bread? Do you need a new recipe for each one?
Not really. You need to understand the principles behind baking. Learn how the balance of ingredients helps in baking. Also, understand the role of heat. These factors can help you produce whatever you want to bake, be it a cupcake or bread.
When it comes to developing a solution, frameworks like Node.js, Flask or Spring Boot are very much like those recipes. They tell you exactly how to write code for specific scenarios. but what happens when the “next big framework” comes along? Do you want to start from scratch every time?
Instead, if you learn the principles like modularity, single responsibility, and testing, you can build any application. You can develop in any framework. This is similar to how a baker who understands the science of baking can adapt to any dish.
I will use the analogy of baking. It will show how learning timeless programming principles allows you to write clean code. This code will be scalable and maintainable, regardless of the framework or language.
Let’s bake (or code) something simple. Imagine we need an API that calculates income tax. We would need to achieve simple 3 things. and they are;
- Fetch tax slabs from a public API.
- Convert the calculated tax into preferred currency.
- Return the tax owed for a given income based on the country.
Lets see how we implement this in Node.js, Python, and Java to demonstrate how the same principles apply, regardless of the framework.
The Core Lesson: From Clean Code by Robert C. Martin
One of the most famous quotes from Clean Code says:
A language provides a set of building blocks, but the principles tell you how to use them properly.
Now let’s break that down. Languages and frameworks provide syntax, libraries, and tools to build things. And Principles teach you to:
- Keep your code organized.
- Make it easy for others (and your future self) to understand.
- Ensure it’s scalable, reusable, and testable.
Single Responsibility Is Like Baking Ingredients
When you bake, each ingredient has a purpose. Flour gives structure. Sugar sweetens. Eggs bind. You wouldn’t want sugar to do the job of flour, or eggs to act as yeast. Similarly, in coding, the Single Responsibility Principle (SRP) highlights that every module or function should do one thing. It should also do it well.
// utils/tax.js
const axios = require('axios');
async function fetchTaxData(country) {
try {
const response = await axios.get(`https://api.taxrates.com/${country}`);
return response.data;
} catch (error) {
throw new Error('Failed to fetch tax data');
}
}
function calculateTax(income, taxSlabs) {
let tax = 0;
for (const slab of taxSlabs) {
if (income > slab.upperLimit) {
tax += (slab.upperLimit - slab.lowerLimit) * slab.rate;
} else {
tax += (income - slab.lowerLimit) * slab.rate;
break;
}
}
return tax;
}
module.exports = { fetchTaxData, calculateTax };
Now if you observer carefully you will notice that each function has a single responsibility. If the tax API changes, you only update fetchTaxData
. If tax laws change, you only update calculateTax
.
let’s take a look at how we achieve the same approach using Python and Java.
# utils/tax.py
import requests
def fetch_tax_data(country):
try:
response = requests.get(f"https://api.taxrates.com/{country}")
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
raise RuntimeError(f"Failed to fetch tax data: {e}")
def calculate_tax(income, tax_slabs):
tax = 0
for slab in tax_slabs:
if income > slab['upper_limit']:
tax += (slab['upper_limit'] - slab['lower_limit']) * slab['rate']
else:
tax += (income - slab['lower_limit']) * slab['rate']
break
return tax
Let’s Compare: What Would SRP Look Like in Python?
Python’s simplicity makes it a great language for beginners, but the principles are the same. In Flask:
- Write one module for fetching tax slabs.
- Write another for fetching currency rates.
- A third module handles calculations.
When a fellow developers say, “Flask makes this easy,” I remind them: It’s not the framework—it’s good design principles.
@Service
public class TaxApiService {
private final RestTemplate restTemplate;
public TaxApiService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public List<TaxSlab> fetchTaxData(String country) {
try {
ResponseEntity<List<TaxSlab>> response = restTemplate.exchange(
"https://api.taxrates.com/" + country,
HttpMethod.GET,
null,
new ParameterizedTypeReference<>() {}
);
return response.getBody();
} catch (RestClientException e) {
throw new RuntimeException("Failed to fetch tax data", e);
}
}
}
public class TaxCalculator {
public double calculateTax(double income, List<TaxSlab> taxSlabs) {
double tax = 0;
for (TaxSlab slab : taxSlabs) {
if (income > slab.getUpperLimit()) {
tax += (slab.getUpperLimit() - slab.getLowerLimit()) * slab.getRate();
} else {
tax += (income - slab.getLowerLimit()) * slab.getRate();
break;
}
}
return tax;
}
}
The TaxApiService
in Java fetches data. Additionally, fetch_tax_data in Python fetches data. The TaxCalculator
in Java handles calculations. Similarly, calculate_tax in Python handles calculations. Just like separating flour and sugar, each class does one job.
Modularity Is Like Baking Techniques
If you watch many Youtube videos by a skilled baker like Natasha from USA, you will notice something. Oh, I love her recipes 😃. Our very own young and upcoming chef ‘Wild Cook’ from Sri Lanka shares this trait. They do not mix all the ingredients all at once. First, they sift the flour. Then, they cream the butter and sugar. Modularity in programming is similar—you separate your code into independent, reusable parts.
In our tax calculator:
- Node.js uses separate utility functions for fetching data and performing calculations.
- Python organizes these into modules or packages.
- Java uses a layered architecture: controllers, services, and repositories.
From The Pragmatic Programmer:
A modular system allows you to swap one piece without breaking the others.
Imagine debugging a monolithic application where tax slabs, exchange rates, and calculations are in one giant file. With modularity, each part stands on its own, making debugging and scaling painless.
// server.js
const express = require('express');
const { fetchTaxData, calculateTax } = require('./utils/tax');
const app = express();
app.use(express.json());
app.post('/calculate-tax', async (req, res) => {
const { country, income } = req.body;
try {
const taxSlabs = await fetchTaxData(country);
const tax = calculateTax(income, taxSlabs);
res.json({ tax });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(3000, () => console.log('Server running on port 3000'));
# app.py
from flask import Flask, request, jsonify
from utils.tax import fetch_tax_data, calculate_tax
app = Flask(__name__)
@app.route('/calculate-tax', methods=['POST'])
def calculate_tax_endpoint():
data = request.json
try:
tax_slabs = fetch_tax_data(data['country'])
tax_amount = calculate_tax(data['income'], tax_slabs)
return jsonify({'tax': tax_amount})
except Exception as e:
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
app.run(debug=True)
@RestController
public class TaxController {
private final TaxApiService taxApiService;
private final TaxCalculator taxCalculator;
public TaxController(TaxApiService taxApiService, TaxCalculator taxCalculator) {
this.taxApiService = taxApiService;
this.taxCalculator = taxCalculator;
}
@PostMapping("/calculate-tax")
public ResponseEntity<?> calculateTax(@RequestBody TaxRequest request) {
try {
List<TaxSlab> taxSlabs = taxApiService.fetchTaxData(request.getCountry());
double tax = taxCalculator.calculateTax(request.getIncome(), taxSlabs);
return ResponseEntity.ok(new TaxResponse(tax));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
}
}
}
Observe the codes very closely. the server doesn’t care how tax data is fetched. It doesn’t care how it is calculated either. The server simply orchestrates the entire process seamlessly and efficiently. Now, even if you try to change the calculation logic to adapt to new tax regulations, the server remains untouched. You can also add different financial methods for different processes without affecting the server.
The controller is modular, delegating tasks to dedicated classes that handle specific functions, ensuring a clear separation of concerns. Each module can be replaced or updated independently. This approach not only streamlines the maintenance process but also significantly enhances the scalability and flexibility of the system.
The approach we followed ensures consistent performance and reliability throughout. It also allows for seamless integration of new features and improvements. This is made possible by its modular architecture. It does not disrupt the overall functionality. This design makes it an ideal solution for dynamic environments where requirements may frequently evolve.
The Oven Temperature Is Error Handling
From The Pragmatic Programmer:
Design for the worst-case scenario.
Just like every cake requires precise baking temperatures, every software project requires thoughtful error handling. What happens if the oven is too hot or an API call fails? Good bakers and developers plan for the unexpected.
Node.js, Python, and Java all handle errors similarly, gracefully catching exceptions and responding appropriately.
@Service
public class TaxService {
private final RestTemplate restTemplate;
public TaxService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public List<TaxSlab> fetchTaxData(String country) throws TaxApiException {
try {
ResponseEntity<List<TaxSlab>> response = restTemplate.exchange(
"https://api.taxrates.com/" + country,
HttpMethod.GET,
null,
new ParameterizedTypeReference<>() {}
);
return response.getBody();
} catch (RestClientException e) {
throw new TaxApiException("Failed to fetch tax data", e);
}
}
}
This layered architecture ensures you can replace the API source or handle new edge cases without touching the calculation logic.
Scalability and Performance
From Designing Data-Intensive Applications:
Always think about how data flows through your system.
An Income Tax Calculator seems simple, but what if:
- You support 10,000+ users simultaneously?
- APIs respond slowly or fail under load?
Here’s how we apply principles like caching and asynchronous processing.
const redis = require('redis');
const client = redis.createClient();
async function fetchTaxData(country, axios) {
const cachedData = await client.getAsync(country);
if (cachedData) {
return JSON.parse(cachedData);
}
const response = await axios.get(`https://api.taxrates.com/${country}`);
const data = response.data;
client.setex(country, 3600, JSON.stringify(data)); // Cache for 1 hour
return data;
}
In high-load scenarios, fetching tax data for every request is inefficient. We can cache API responses using Redis:
Testing
Code that isn’t tested is broken by design.
Why Test?
Testing ensures your code works as intended and continues to work as it grows. The logic remains the same across languages. Tests focus on functionality, not framework specifics.
test('calculate tax correctly', () => {
const taxSlabs = [...];
const income = 25000;
expect(calculateTax(income, taxSlabs)).toBe(4500);
});
Use Jest or Mocha to test small functions. Change from Express to Koa? The tests for your logic remain the same.
def test_calculate_tax():
tax_slabs = [...]
income = 25000
assert calculate_tax(income, tax_slabs) == 4500
Pytest lets you test each function independently. Swap Flask for FastAPI, and the tests for your business logic don’t need rewriting.
@Test
void testCalculateTax() {
List<TaxSlab> taxSlabs = ...;
double income = 25000;
assertEquals(4500, taxService.calculateTax(taxSlabs, income));
}
JUnit ensures that your service logic is correct, regardless of whether you’re running it under Spring Boot or another framework.
Principles Are the Real Recipe
Whether you’re baking cakes or coding software, principles are the real skill set. Frameworks (or recipes) provide assistance. Frameworks will come and go. Today it’s Flask, tomorrow it might be FastAPI. Today it’s Spring Boot, tomorrow it might be Quarkus. But principles – those stay constant.
If you understanding concepts like SRP, modularity, and error handling allows you to adapt to any situation.
Let’s bake (or code) something great!
Discover more from Amal Gamage
Subscribe to get the latest posts sent to your email.