Java: Primitive Data Types vs Wrapper Classes

In the previous post, we looked at the difference between long (a primitive) and Long (its wrapper class). That was just one example — but in fact, every primitive type in Java has a wrapper class.

So in this post, let’s zoom out and cover the bigger picture:

  • What are primitive data types?
  • What are wrapper classes?
  • How are they initialized?
  • What are the benefits, differences, and when should you use one over the other?

By the end, you’ll have a simple rule of thumb that will save you from confusion: stick to primitives by default, use wrappers only when you need object features.

1. Primitive Data Types

Primitives are the basic building blocks of data in Java. They are not objects and store their values directly in memory (usually on the stack).

Java provides 8 primitive types:

  • byte, short, int, long (integers)
  • float, double (floating-point numbers)
  • char (character)
  • boolean (true/false)

Example:

int number = 10;
boolean isActive = true;

They are fast, memory-efficient, and always hold an actual value.

2. Wrapper Classes

For every primitive, Java provides a corresponding wrapper class in the java.lang package. These are objects that “wrap” a primitive inside a class. • Byte, Short, Integer, Long • Float, Double • Character • Boolean

Integer numberObj = Integer.valueOf(10);
Boolean isActiveObj = Boolean.TRUE;

Wrappers are essential when:

  • Working with Collections (e.g., List<Integer> instead of List<int>).
  • You need to represent null (absence of a value).
  • You want to use utility methods (like parsing strings into numbers).

2.5 Initializing Primitives vs Wrappers

Primitive Initialization

  • Direct and straightforward.
  • Local variables must be initialized before use.
  • Class fields get a default value (int → 0, boolean → false).
int x = 10;        // explicit initialization
boolean flag;      // flag must be assigned before use

Wrapper Initialization

  • Wrappers are objects, so they can be null.
  • Default value for wrapper fields is null.
  • Different ways to initialize:
Integer a = new Integer(10);     // old (not recommended)
Integer b = Integer.valueOf(10); // preferred
Integer c = 10;   // autoboxing (simplest)

Similar but Different

  • int x = 0; → raw value stored directly.
  • Integer y = 0; → an object reference pointing to an Integer.

So while syntax can look similar, the memory model and behavior are not the same.

3. Key Differences

Primitive Wrapper Class
Stored directly in memory (stack) Stored as an object reference (heap)
Faster and more memory-efficient Slightly slower, more memory use
Cannot be null Can be null
No methods available Comes with utility methods
Value can be reassigned directly Immutable object (once created, can’t be changed)

4. Autoboxing & Unboxing

Java makes conversion between primitives and wrappers seamless.

  • Autoboxing: primitive → wrapper
  • Unboxing: wrapper → primitive
Integer obj = 5;   // autoboxing
int num = obj;     // unboxing

This is convenient, but can introduce performance overhead if overused.

5. Benefits of Wrapper Classes

  • Collections & Generics: You can’t store int in a List, but you can store Integer.
List<Integer> numbers = new ArrayList<>();
numbers.add(5);
  • Utility Methods:
int parsed = Integer.parseInt("123");
  • Null Handling: Sometimes you need null to represent “no value”.

6. When to Use

  • Primitives → default choice. Use them when performance matters (loops, counters, math).
  • Wrappers → when you need:
    • Collections
    • Nullability
    • Utility methods

6.5 Rule of Thumb

  • Default to primitives – they are faster, memory-friendly, and straightforward.
  • Use wrappers only when necessary, such as:
    • You need to store them in Collections / Generics.
    • You need to represent null (e.g., database values).
    • You want to leverage utility methods (Integer.parseInt, Boolean.valueOf, etc.).

👉 In short: always use primitive unless there’s a clear reason to use the wrapper.

⚡ Bonus: What About String?

If you’re wondering where String fits in — it’s not a primitive, nor a wrapper. String is a regular class in java.lang, but Java gives it special treatment so it behaves almost like a primitive in many cases.

For example:

String name = "Hazriq";

looks as simple as assigning an int or boolean. But under the hood, String is an object, immutable, and stored differently from primitives.

This is why you can do things like:

int length = name.length();   // methods available

So:

  • Primitives = raw values
  • Wrappers = object versions of primitives
  • String = class, not a primitive, but commonly treated as a “basic type” in day-to-day Java

7. Best Practices & Gotchas

  • Avoid unnecessary boxing/unboxing in performance-critical code.
  • Be careful comparing wrappers:
Integer a = 1000;
Integer b = 1000;

System.out.println(a == b);      // false (different objects)
System.out.println(a.equals(b)); // true
  • Remember: wrapper classes and String are immutable. Once created, their value never changes — any “modification” actually creates a new object. (We’ll explore immutability in depth in a future post.)

Primitives are simple and fast, wrappers are flexible and object-friendly, and String is a special class that feels primitive but isn’t.

Understanding when to use each is one of those small but important skills that makes you write cleaner and more efficient Java code.

September 27, 2025 · 4 min

Java: Long vs long

Recently, I got a PR review comment that made me pause. It was about something I thought I already knew well: choosing between long and Long in Java.

And honestly, it hit differently because of how my new company approaches engineering.

In my previous company, the priority was speed. We had the luxury of pushing features straight to production quickly. Optimization, memory efficiency, and cost tuning weren’t the main focus. The mission was simple: deliver fast, and move on.

But in my new company, the approach is different. We take more time to build the right way — thinking about memory, cost, long-term maintainability, and performance.

For someone like me with 8 years of experience, this shift has been an eye-opener. It’s one thing to “make it work.” It’s another thing entirely to “make it work well.”

Which brings me back to… long vs Long.

Primitive vs Wrapper: A Quick Refresher

Java is a bit different from languages like Python or JavaScript. It has two “flavors” of types:

  • Primitives: raw values like int, long, boolean.
  • Wrapper Classes: object versions of these primitives: Integer, Long, Boolean.

This distinction often feels academic at first, but it has real consequences in how your program behaves.

So what’s the actual difference?

  • long:

    • A primitive 64-bit value.
    • Default value: 0.
    • Lightweight and memory efficient.
    • Cannot be null.
  • Long:

    • A wrapper class around long.
    • Default value (when uninitialized in an object): null.
    • Heavier — since it’s an object, it lives on the heap.
    • Can be used in places where only objects are allowed (like List<Long>).

Autoboxing and Unboxing

One of the reasons developers sometimes overlook the difference between long and Long is because Java silently converts between them. This feature is called autoboxing and unboxing.

  • Autoboxing: automatically converting a primitive (long) into its wrapper (Long).
  • Unboxing: automatically converting a wrapper (Long) back into its primitive (long).

This allows you to write code that looks simple:

Long a = 5L;   // autoboxing: primitive long -> Long object
long b = a;    // unboxing: Long object -> primitive long

Without autoboxing, you’d have to do this manually:

Long a = Long.valueOf(5L);   // boxing
long b = a.longValue();      // unboxing

Pretty verbose, right? That’s why Java added this feature in Java 5 — to make our lives easier.

The convenience comes with a trade-off:

  • Performance cost: Each conversion creates extra instructions, and sometimes even new objects. In a loop that runs millions of times, those hidden allocations can hurt performance.
  • Null safety: If you try to unbox a Long that’s actually null, you’ll get a NullPointerException. For example:

This is why being deliberate about whether you use long or Long matters.

In short:

  • Autoboxing and unboxing make your code cleaner.
  • But they also hide potential pitfalls in performance and null handling.

When to use which

Here’s a practical rule of thumb I picked up from the review:

  • Use long when you need raw performance, don’t care about null, and the value is always expected to be present.
  • Use Long when you need:
    • Nullability (e.g., a database field that may not be set).
    • To work with Generics or Collections (List<Long> won’t work with primitives).

Closing

This wasn’t just a lesson about long vs Long. It was a reminder that context matters. In a fast-moving environment, you might get away with just shipping things, but in an environment where optimization, cost, and maintainability matter, these small details make a big difference.

For me, this was an eye-opener even after 8 years in software development. The fundamentals are still powerful, and sometimes revisiting them is the best way to level up.

The next time you see long vs Long, pause for a moment. Is null a possibility? Do you need collection support? Or do you just need a fast, simple number?

That little decision can make your codebase more consistent, more efficient, and less bug-prone.

September 23, 2025 · 4 min

AWS: DynamoDB & IA Class

If you’ve been using Amazon DynamoDB for a while, you’ve probably noticed something: not all your data gets the same attention. Some of it is “hot” — frequently accessed, constantly updated. But a lot of it is “cold” — just sitting there, costing you money every month.

What if you could store that cold data somewhere cheaper without changing your code or losing availability?

That’s exactly what DynamoDB Standard-IA (Infrequent Access) is for. In this post, we’ll break down what it is, how it works, how it can save you money, and when it might not be the best idea.

Recap on previous post:

DynamoDB Table Classes

DynamoDB offers different table classes to optimize costs based on your access patterns:

  1. Standard – For data you access frequently.
    • Designed for low-latency access any time.
    • Best for your main, active application data.
    • The default table class when you create a new DynamoDB table.
    • Suitable for most workloads. Provides high availability and durability.
  2. Standard-IA (Infrequent Access) – For data you rarely read or write.
    • Designed for data that is not accessed often but needs to be available when needed.
    • Offers lower storage costs compared to Standard.
    • Higher retrieval costs, so it’s best for data that you access less than once a month.

Both table classes work exactly the same way from a developer’s point of view:

  • Same APIs
  • Same queries
  • Same AWS Console experience

The only difference? How AWS stores it behind the scenes and how much you pay.

What is Standard-IA?

Standard-IA is a table class designed for data that you access infrequently. It’s like a storage locker for your cold data — it’s still there when you need it, but it costs less to keep it around.

Think of it like moving your old books to a basement shelf:

  • They’re still yours.
  • You can still get them any time.
  • But they’re not taking up expensive prime shelf space.

How can it save you money?

The main savings come from storage pricing:

Storage Class Price per GB/month
Standard ~$0.25
Standard-IA ~$0.10

That’s about 60% cheaper for storage.

Example: If you have 100 GB of archived order history:

  • Standard = ~$25/month
  • Standard-IA = ~$10/month

💡 That’s $15/month saved — or $180/year — just for one table.

The Catch

But it’s not all sunshine and rainbows. There are some important trade-offs to consider:

  • Retrieval Costs – Around $0.01 per GB each time you read data from IA.
  • Minimum 30-Day Storage Billing – You pay for at least 30 days even if you delete earlier.
  • Not for Hot Data – If accessed often, retrieval fees can eat up savings.
  • Whole-Table Setting – You can’t mix Standard and IA in one table.

Best Practice before Switching

  • Check Access Patterns — Use CloudWatch metrics to see how often the table is read.
  • Move Predictable Cold Data — Avoid sudden spikes in retrieval.
  • Test on a Smaller Table First — See if retrieval costs are low enough to justify the switch.
  • Combine With TTL — Automatically delete expired data to save more.

DynamoDB Standard-IA is like a budget-friendly storage locker for data you still need but rarely touch. It can cut storage costs by more than half — but only if you choose the right workloads.

Rule of thumb: If it’s predictable, cold, and still worth keeping — IA is your friend.

August 10, 2025 · 3 min

AWS: DynamoDB & DAX Cost Factors

We discussed about DynamoDB in previous post, but let’s dive deeper into the cost factors associated with DynamoDB and its accelerator, DAX (DynamoDB Accelerator).

Recap

  • Amazon DynamoDB (DDB) is AWS’s fully managed NoSQL database service, designed for applications that require consistent performance at any scale.

  • Amazon DynamoDB Accelerator (DAX) is an in-memory caching service for DynamoDB. Think of it as a turbocharger — it reduces read latency from milliseconds to microseconds by storing frequently accessed data in memory.

Together, DDB and DAX can significantly improve application performance — but they also come with different cost models you’ll want to understand before adopting.

When to Use DAX?

DAX is particularly useful when:

  • Your workload has high read traffic with repeated queries for the same items.
  • You want microsecond read latency for real-time user experience.
  • You aim to offload read traffic from DynamoDB to reduce provisioned read capacity usage.

Example: A database for AI model training, where the same training data is accessed repeatedly.

Skip DAX when:

  • Your workload is write-heavy with low read repetition.
  • Your queries are strongly consistent (DAX only supports eventually consistent reads).
  • Your access patterns are highly dynamic and unpredictable — the cache hit rate might be low.

Understanding DynamoDB Costs

DynamoDB costs come from three main areas:

  • Reading Data
    • Imagine a reading allowance — every time you check a page from a book, it uses part of your allowance.
    • You can either pay per read (On-Demand) or buy a monthly “reading subscription” (Provisioned) if you know your usual usage.
  • Writing Data
    • Adding or updating books also uses an allowance — think of it as your “writing subscription” or “per-write” payment.
  • Storing Data
    • This is your bookshelf space.
    • Regular storage (Standard) is always ready but costs more.
    • Cheaper storage (Standard-IA) is for books you rarely read, but you’ll pay a small fee each time you take one.

Extras You Might Pay For:

  • Backups — like taking daily photos of your bookshelf.
  • Copies in other regions — like having the same library in multiple cities.

Understanding DAX Costs

  1. DAX Costs DAX pricing is per node-hour, depending on node type:
  • Smallest node (dax.t3.small) is the cheapest, suitable for dev/test.
  • Larger nodes (dax.r5.large, etc.) cost more but handle higher throughput.
  • DAX clusters require at least 3 nodes for fault tolerance in production.

Note: DAX charges are separate from DynamoDB — even if your reads come from the cache.

Cost Comparison

Component Without DAX (Provisioned) With DAX (Provisioned)
Read Capacity Cost High (due to all reads hitting DDB) Lower (fewer RCUs needed)
Write Capacity Cost Same Same
Storage Cost Same Same
DAX Cost $0 Node-hour charges

If your cache hit rate is low, DAX might increase costs without much benefit.

Final Thoughts

  • Use DAX if you have heavy, repeated reads and need lightning-fast results.
  • Use Standard-IA storage for rarely accessed data — but don’t forget the retrieval cost.
  • Always measure first: monitor read/write usage and cache hit rates before committing.
August 10, 2025 · 3 min

AWS: SAM Introduction

Serverless is one of the most exciting ways to build modern cloud applications — and AWS SAM makes it even easier.

What is AWS SAM?

AWS Serverless Application Model (SAM) is an open-source framework that helps you build and deploy serverless applications on AWS.

It’s designed to simplify your infrastructure-as-code, especially when working with:

  • AWS Lambda
  • API Gateway
  • DynamoDB
  • EventBridge, SQS, Step Functions, and more

At its core, SAM is just a shorthand syntax for AWS CloudFormation — making your templates cleaner, easier to write, and faster to iterate.

Why Use AWS SAM?

You should consider SAM when:

  • You’re building serverless applications using AWS services like Lambda and API Gateway.
  • You want to define infrastructure as code but find raw CloudFormation too verbose.
  • You want to test Lambda functions locally using Docker.
  • You prefer guided deployment over manually zipping and uploading code.

SAM is ideal for:

  • Quick prototyping of serverless apps
  • Developer teams who want simplicity without giving up AWS-native IaC
  • Learning how serverless works with real AWS infrastructure

SAM vs. CloudFormation: what’s the difference?

SAM is built on top of CloudFormation, so it inherits all the benefits of CloudFormation while providing a simpler syntax for serverless applications. Here are some key differences:

Feature AWS CloudFormation AWS SAM
Purpose Define any AWS infrastructure Focused on serverless apps
Syntax YAML/JSON (verbose) Simplified YAML with shorthand
Testing ❌ No built-in local testing ✅ Local testing with Docker
Deployment CLI aws cloudformation deploy sam deploy –guided
Abstraction Layer Base layer Built on top of CloudFormation

In short: SAM is CloudFormation — just way easier for serverless.

You still get all the benefits of CloudFormation (rollback, drift detection, etc.), but with less effort and boilerplate.

SAM Main Components

  1. template.yaml

Your SAM template is the blueprint of your application — it declares all the AWS resources your app needs.

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.12
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get
  1. samconfig.toml: Configuration for Reusability & Environments

When you run sam deploy --guided, SAM generates a samconfig.toml file. This file stores deployment settings like your S3 bucket, stack name, region, and parameter overrides — so you don’t need to type them every time.

But beyond that, you can define multiple environments using named configurations. Example:

version = 0.1
[staging.deploy.parameters]
stack_name = "my-sam-app-staging"
region = "ap-southeast-1"
s3_bucket = "my-sam-artifacts-staging"
capabilities = "CAPABILITY_IAM"
parameter_overrides = "Environment=staging"

[prod.deploy.parameters]
stack_name = "my-sam-app-prod"
region = "ap-southeast-1"
s3_bucket = "my-sam-artifacts-prod"
capabilities = "CAPABILITY_IAM"
parameter_overrides = "Environment=prod"

Now you can deploy using:

sam deploy --config-env staging
sam deploy --config-env prod

This allows:

  • Cleaner separation between dev/staging/prod
  • Safer deployment practices
  • Per-env overrides for Lambda environment variables, tags, etc.
  1. SAM CLI

A command-line tool that simplifies development and deployment:

  • sam init – scaffold a new project
  • sam build – package your code
  • sam deploy – push it to AWS
  • sam local invoke – test individual functions locally
  • sam local start-api – emulate full API locally

If you’re starting your journey into serverless with AWS, SAM is one of the best tools to learn and use. It removes the friction of writing raw CloudFormation, supports local development, and lets you ship your ideas quickly.

It’s not just beginner-friendly — it’s also powerful enough to be used in production systems, especially when paired with other AWS services like DynamoDB, Step Functions, and EventBridge.

August 3, 2025 · 3 min

AWS: Lambda Basics

AWS Lambda is one of the most exciting services in the serverless world. It lets you write code that automatically responds to events — without needing to worry about provisioning servers or managing infrastructure.

In this post, I will cover the basics:

  • What is Lambda?
  • What are the core components?
  • Why use it?
  • A real use case: processing SQS messages in TypeScript
  • Common limitations

What is AWS Lambda?

AWS Lambda is a serverless compute service that lets you run code in response to events without provisioning or managing servers. You can use Lambda to run code for virtually any type of application or backend service with zero administration. Just upload your code and Lambda takes care of everything required to run and scale your code with high availability.

You don’t manage servers. You just focus on the code, and Lambda takes care of:

  • Running it
  • Scaling it automatically
  • Charging you only when it runs

Key Components of AWS Lambda

  1. Handlers: The entry point for your Lambda function. It’s the method that AWS Lambda calls to start execution.
  2. Events: Lambda functions are triggered by events, which can come from various AWS services like S3, DynamoDB, API Gateway, or even custom events.
  3. Context: Provides runtime information to your Lambda function, such as the function name, version, and remaining execution time.
  4. IAM Roles: AWS Identity and Access Management (IAM) roles define the permissions for your Lambda function, allowing it to access other AWS services securely.
  5. Environment Variables: Key-value pairs that you can use to pass configuration settings to your Lambda function at runtime.
  6. Timeouts and Memory: You can configure the maximum execution time and memory allocated to your Lambda function, which affects performance and cost.
  7. CloudWatch Logs: Automatically logs the output of your Lambda function, which you can use for debugging and monitoring.

Why Use AWS Lambda?

  • Cost-Effective: You only pay for the compute time you consume. There are no charges when your code is not running.
  • Scalability: Automatically scales your application by running code in response to each event, so you don’t have to worry about scaling your infrastructure.
  • Flexibility: Supports multiple programming languages (Node.js, Python, Java, Go, C#, Ruby, and custom runtimes) and can be used for a wide range of applications, from simple scripts to complex microservices.
  • Event-Driven: Easily integrates with other AWS services, allowing you to build event-driven architectures that respond to changes in your data or system state.
  • Zero Administration: No need to manage servers or runtime environments. AWS handles all the infrastructure management tasks, including patching, scaling, and availability.

Real Use Case: TypeScript Lambda to Process SQS → DynamoDB

In my current role, we use AWS Lambda to process messages from SQS queues. Here’s a simple example of how you can set up a Lambda function in TypeScript to process messages from an SQS queue and store them in DynamoDB.

Lets say we receive messages in SQS that contain user data, and we want to store this data in DynamoDB.

import { SQSHandler, SQSEvent, Context } from "aws-lambda";
import { DynamoDB } from "aws-sdk";

const dynamoDb = new DynamoDB.DocumentClient();

export const handler: SQSHandler = async (
  event: SQSEvent,
  context: Context
) => {
  for (const record of event.Records) {
    const userData = JSON.parse(record.body);
    const params = {
      TableName: "Users",
      Item: userData,
    };
    await dynamoDb.put(params).promise();
  }
};

In this example:

  • We import necessary types from aws-lambda and the DynamoDB client from aws-sdk.
  • The handler function processes each message in the SQS event.
  • We parse the message body and store it in a DynamoDB table named Users.

This function will be uploaded to AWS Lambda, and you can configure it to trigger whenever new messages arrive in the SQS queue.

Common Limitations of AWS Lambda

  • Execution Time: Lambda functions have a maximum execution time of 15 minutes. If your task takes longer, you may need to break it into smaller functions or use other services.
  • Cold Starts: When a Lambda function is invoked after being idle, it may take longer to start due to the initialization time (cold start). This can affect performance, especially for latency-sensitive applications.
  • Limited Resources: Each Lambda function has a maximum memory limit (up to 1024 MB) and a maximum package size (50 MB for direct upload, 250 MB when using layers). This can be a constraint for resource-intensive applications.
  • Limited Runtime Environment: While Lambda supports multiple programming languages, you may encounter limitations with certain libraries or dependencies that require a specific runtime environment.
  • State Management: Lambda functions are stateless, meaning they do not retain any state between invocations. If you need to maintain state, you will have to use external storage solutions like DynamoDB or S3.
  • Concurrency Limits: There are limits on the number of concurrent executions for Lambda functions. If your application experiences a sudden spike in traffic, you may hit these limits, leading to throttling of requests.
  • Vendor Lock-In: Using AWS Lambda ties you to the AWS ecosystem, which can make it challenging to migrate to other cloud providers or on-premises in the future.

Wrap-Up

AWS Lambda is a powerful tool for building serverless applications that can scale automatically and respond to events without the need for server management. By understanding its core components and limitations, you can effectively leverage Lambda to build efficient, cost-effective applications that meet your business needs.

Whether you’re processing SQS messages, building APIs with API Gateway, or integrating with other AWS services, Lambda provides a flexible and scalable solution that can adapt to your application’s requirements.

July 27, 2025 · 5 min

AWS: DynamoDB Basics

Datastore is always a crucial part of any application, and choosing the right database can significantly impact your application’s performance, scalability, and maintainability. In this post, we’ll explore AWS DynamoDB.

Database Types

There are two main types of databases:

  • Relational Databases (RDBMS): These databases use structured query language (SQL) and are designed to handle structured data with predefined schemas. Examples include MySQL, PostgreSQL, and Oracle.
  • NoSQL Databases: These databases are designed to handle unstructured or semi-structured data. They provide flexibility in data modeling and can scale horizontally. Examples include MongoDB, Cassandra, and DynamoDB.

If you’re coming from MySQL or PostgreSQL, imagine removing JOINs and replacing rows with JSON-like documents stored under a single key.

Key Features of DynamoDB

  • Fully Managed: DynamoDB is a fully managed service, meaning AWS handles the operational aspects such as hardware provisioning, setup, configuration, and scaling.
  • Performance at Scale: It automatically scales up and down to adjust for capacity and maintain performance.
  • Flexible Data Model: Supports key-value and document data structures, allowing for a variety of use cases.
  • Built-in Security: Offers encryption at rest and in transit, along with fine-grained access control.

So, what is DynamoDB? DynamoDB is a fully managed NoSQL database service provided by AWS that offers fast and predictable performance with seamless scalability. It is designed to handle large amounts of data and high request rates, making it ideal for applications that require low-latency data access.

Key Concepts

  • Tables: The primary structure in DynamoDB, similar to tables in relational databases. Each table has a primary key that uniquely identifies each item.
  • Items: Individual records in a table, similar to rows in a relational database.
  • Attributes: The data fields in an item, similar to columns in a relational database.
  • Primary Key: Uniquely identifies each item in a table. It can be a simple primary key (partition key) or a composite primary key (partition key and sort key).
  • Indexes: Allow for efficient querying of data. DynamoDB supports both global secondary indexes (GSI) and local secondary indexes (LSI).

A simple item in a DynamoDB table might look like this:

{
  "UserId": "12345", # Unique identifier for the user. Primary key.
  "Name": "Hazriq",
  "Email": "hazriq@example.com"
}

Benefits of Using DynamoDB

  • Scalability: Automatically scales to handle large amounts of data and high request rates without manual intervention.
  • Performance: Provides low-latency data access, making it suitable for real-time applications.
  • Flexibility: Supports various data models, allowing developers to choose the best fit for their application.
  • Cost-Effective: Pay-as-you-go pricing model, where you only pay for the resources you use, making it cost-effective for applications with variable workloads.
  • Integration with AWS Services: Seamlessly integrates with other AWS services like Lambda, API Gateway, and CloudWatch for monitoring and logging.
  • TTL (Time to Live): Automatically deletes expired items, helping manage storage costs and data lifecycle.

Performance Considerations

  • Provisioned Throughput: You can specify the read and write capacity units for your table, which determines how many reads and writes per second your table can handle.
  • On-Demand Capacity: Automatically scales to accommodate workload changes, making it suitable for unpredictable workloads.
  • Caching: Use DynamoDB Accelerator (DAX) for in-memory caching to improve read performance for read-heavy workloads.
  • Batch Operations: Use batch operations for efficient processing of multiple items in a single request, reducing the number of round trips to the database.

Without DAX

  • Reads and writes are directly from the DynamoDB table.
  • Each read or write operation incurs a latency based on the network and DynamoDB’s processing time.

With DAX

  • DAX acts as an in-memory cache, reducing the latency for read operations.
  • DAX handles cache misses by fetching data from DynamoDB and storing it in memory for subsequent requests.
  • This significantly speeds up read operations, especially for frequently accessed data.

When not to Use DynamoDB

While DynamoDB is a powerful tool, it may not be the best fit for every use case. Here are some scenarios where you might consider alternatives:

  • Complex Queries: If your application requires complex queries with multiple joins or aggregations, a relational database might be more suitable.
  • Transactional Support: If your application requires complex transactions involving multiple items or tables, consider using a relational database or a database that supports multi-item transactions.
  • Large Binary Objects: If your application needs to store large binary objects (BLOBs), such as images or videos, consider using Amazon S3 for storage and DynamoDB for metadata.
  • High Write Throughput: If your application requires extremely high write throughput, consider using Amazon S3 or a distributed database like Apache Cassandra.

DynamoDB shines when you need a fast, scalable, and fully managed database that just works — whether you’re powering a real-time leaderboard, handling millions of API requests, or storing user sessions with minimal latency. By understanding its core concepts and performance features like DAX, you can unlock a powerful tool that fits right into modern, serverless-first architectures.

Of course, like any tool, it’s not a one-size-fits-all solution. Knowing when and how to use DynamoDB effectively is key — and that journey starts with grasping its strengths.

July 20, 2025 · 4 min

AWS: SQS vs SNS

At my new company, we rely heavily on AWS SQS (Simple Queue Service) and SNS (Simple Notification Service) to handle the large volume of records we need to ingest daily.

Think of a situation where the system experiences a temporary bottleneck or needs to go offline for maintenance or upgrades. In such cases, SQS acts as a buffer—safely storing incoming records in a queue so they’re not lost. Even during traffic spikes, the messages are queued and processed at our own pace. For example, our AWS Lambda function polls the queue and retrieves messages when it’s ready, allowing the system to remain responsive and scalable even under pressure.

Core Components of SQS and SNS

To better understand how SQS and SNS work, it helps to break down their main components.

📨 SQS Main Components:

  • Queue: The main container where messages are stored temporarily until processed.
  • Producer: The system or service that sends messages to the queue.
  • Consumer: The service (e.g. Lambda, EC2) that polls the queue and processes the message.
  • Visibility Timeout: A short period where the message becomes invisible to other consumers once picked up—helps avoid duplicate processing.
  • DLQ (Dead-Letter Queue): A separate queue that stores messages that couldn’t be successfully processed after several retry attempts. Useful for debugging failed messages.

📣 SNS Main Components:

  • Topic: The central component that receives messages from publishers.
  • Publisher: The producer that sends a message to a topic.
  • Subscriber: Services or endpoints (like Lambda, SQS queue, HTTPS endpoint, email) that receive messages pushed from the topic.
  • Subscription Filter Policy: You can apply rules to decide which subscribers should receive which messages (useful for message routing).

SQS vs SNS – What’s the Difference?

While both SQS and SNS are messaging services provided by AWS, they serve very different purposes:

Feature SQS (Simple Queue Service) SNS (Simple Notification Service)
Message Pattern Point-to-point (Queue-based) Publish/Subscribe (Fan-out)
Delivery Target Message goes to one consumer Message goes to multiple subscribers
Storage Messages are stored temporarily in a queue Messages are pushed immediately, not stored by default
Use Case Decouple producer and consumer; reliable message handling Broadcast messages to multiple endpoints/services
Consumer Behaviour Consumers poll the queue Subscribers receive push notifications
  • Use SQS when you want to decouple your producer and consumer, especially if the consumer might be temporarily unavailable.
  • Use SNS when you want to broadcast messages to multiple services (e.g., notify a Lambda, an email service, and an HTTP endpoint at the same time).
July 15, 2025 · 2 min

JS: Express.js

Express.js is a minimalist and flexible Node.js web application framework that provides a robust set of features for building web and mobile applications and APIs. If you’re looking to build fast, scalable, and efficient server-side applications with JavaScript, Express.js is your go-to choice. Its unopinionated nature gives you immense freedom,

Setting Up

Getting started with Express.js is straightforward. If you already have a Node.js project, or if you’re starting a new one, follow these simple steps:

  • Initializing a Node.js project (npm init).
  • Installing Express.js (npm install express).

Now, you’re ready to create your first Express server!

Core Concepts

Understanding these fundamental concepts is key to mastering Express.js:

Routing

Routing is the process of determining how an application responds to a client request to a particular endpoint, which is a URI (or path) and a specific HTTP request method (GET, POST, PUT, DELETE, etc.).

In Express.js, you define routes using methods corresponding to HTTP verbs on the app object (an instance of Express).

HTTP Methods (Verbs): These map directly to common operations:

  • GET: Retrieve data (e.g., fetching a list of users).
  • POST: Submit data to be processed (e.g., creating a new user).
  • PUT: Update existing data (e.g., modifying a user’s details).
  • DELETE: Remove data (e.g., deleting a user).
const express = require("express");
const app = express();
const port = 3000;

// GET request to the root URL
app.get("/", (req, res) => {
  res.send("Hello from Express!");
});

// GET request to /users
app.get("/users", (req, res) => {
  res.json([
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" },
  ]);
});

// POST request to /users
app.post("/users", (req, res) => {
  // Logic to create a new user
  res.status(201).send("User created successfully!");
});

app.listen(port, () => {
  console.log(`Server listening at http://localhost:${port}`);
});
// Route Parameters:
// Example URL: /users/123
app.get("/users/:id", (req, res) => {
  const { id } = req.params;
  res.send(`Fetching user with ID: ${id}`);
});

// Query Parameters:
// Example URL: /search?q=nodejs&category=backend
app.get("/search", (req, res) => {
  const { q: searchTerm, category } = req.query;
  res.send(`Searching for "${searchTerm}" in "${category}" category.`);
});

Middleware

Middleware functions are core to Express.js. They are functions that have access to the request object (req), the response object (res), and the next() function in the application’s request-response cycle. The next() function is crucial as it passes control to the next middleware function.

Think of middleware as a series of steps a request goes through before reaching its final route handler.

  • How Middleware Works: Requests flow sequentially through middleware functions. Each function can perform operations, modify req or res, end the cycle by sending a response, or pass control to the next middleware via next().

  • Common Use Cases:

    • Logging: Recording details about incoming requests.
    • Authentication/Authorization: Verifying user credentials and permissions.
    • Body Parsing: Express itself doesn’t parse request bodies by default. Middleware like express.json() and express.urlencoded() are essential for handling JSON and URL-encoded form data.
    • Serving Static Files: Handled by express.static().
    • Error Handling: Special middleware for catching and processing errors.

Example of Custom Middleware:

// A simple logger middleware
const logger = (req, res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  next(); // Pass control to the next handler
};

app.use(logger); // Use the middleware for all incoming requests

app.get("/", (req, res) => {
  res.send("Home Page with Logger!");
});

Handling Request (req) and Response (res)

  • req (Request Object):
    • Accessing request headers (req.header).
    • Accessing route and query parameters (req.params, req.query).
    • Accessing the request body (req.body).
  • res (Response Object):
    • Sending various types of responses: res.send(), res.json(), res.sendFile().
    • Setting HTTP status codes: res.status().
    • Redirecting requests: res.redirect().
    • Chaining methods (e.g., res.status(200).json(...)).

Typical Project Structure

While Express is unopinionated, a well-organized project structure is crucial for maintainability and scalability, especially as your application grows. Here’s a common pattern:

my-express-app/
├── node_modules/
├── server.js             # Entry point: Starts the server, sets up environment
├── app.js                # Express app configuration: Defines middleware, connects routes
├── routes/               # Defines API endpoints and links to controllers
│   ├── auth.routes.js
│   └── user.routes.js
├── controllers/          # Contains request handlers: Logic for specific routes, interacts with services
│   ├── auth.controller.js
│   └── user.controller.js
├── services/             # Business logic: Handles complex operations, orchestrates data flow
│   ├── auth.service.js
│   └── user.service.js
├── models/               # Data schema definitions (e.g., Mongoose schemas, Sequelize models)
│   ├── User.js
│   └── Product.js
├── middlewares/          # Custom middleware functions
│   ├── auth.middleware.js
│   └── error.middleware.js
├── config/               # Configuration files (database settings, environment variables)
│   └── db.config.js
├── package.json
├── package-lock.json
└── .env                  # Environment variables

Sample Project

( coming soon )

June 23, 2025 · 4 min

PY: defaultdict()

A defaultdict is a subclass of dict that calls a factory function to supply missing values for any requested key.

… In today’s call, I was looking at a simple python solution that will try to count the number of fruits based on types.

Lets take this example. I want to know how many numbers apple in the list.

The typical way:

fruit_list = ["apple", "banana", "apple", "orange", "banana", "apple"]
fruit_counts = {}

for fruit in fruit_list:
    if fruit in fruit_counts:
        fruit_counts[fruit] += 1
    else:
        fruit_counts[fruit] = 1

print(fruit_counts)
# Output: {'apple': 3, 'banana': 2, 'orange': 1}

We will check the fruit_counts dict. If it’s there, add more. If not, set it it to 1.

Looks simple, but apparently… There’s a way to do this in more pythonic way..

from collections import defaultdict

fruit_list = ["apple", "banana", "apple", "orange", "banana", "apple"]
fruit_counts = defaultdict(int) # defaultdict with a default factory of int (which returns 0)

for fruit in fruit_list:
    fruit_counts[fruit] += 1 # If 'fruit' is not in fruit_counts, it defaults to 0, then 1 is added.

print(fruit_counts)
# Output: defaultdict(<class 'int'>, {'apple': 3, 'banana': 2, 'orange': 1})

If fruit is already a key in fruit_counts, its current value is incremented by 1. If fruit is not yet a key in fruit_counts, defaultdict(int) automatically creates fruit_counts[fruit] and initializes its value to 0. Then, 1 is added to it, making its value 1.

From GeeksforGeeks:

  • Using int: If you use int as the factory function, the default value will be 0 (since int() returns 0).
  • Using list: If you use list as the factory function, the default value will be an empty list ([]).
  • Using str: If you use str, the default value will be an empty string (’’).

Now… What if I want to start from 100? Apparently… we can! via lambda…

from collections import defaultdict

fruit_list = ["apple", "banana", "apple", "orange", "banana", "apple"]

fruit_counts = defaultdict(lambda: 50)

for fruit in fruit_list:
    # If 'fruit' is not in fruit_counts, it defaults to 50, then 1 is added.
    fruit_counts[fruit] += 1

print(fruit_counts)
# defaultdict(<function <lambda> at 0x7f73165e7d30>, {'apple': 53, 'banana': 52, 'orange': 51})
June 11, 2025 · 2 min