Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Java Data Types

Java Data Types

In the previous tutorial we discussed Java Variables and learned that every variable must have three things: a type, a name, and a value. The type (also called data type) tells Java what kind of data the variable can hold and how much memory to allocate for it. In this tutorial, we will take a deep dive into Java data types.

Quick recap on variables:

public static void main(String[] args) {
    // type   name        value
    String firstName = "Folau";
    int    age       = 30;
    double salary    = 85000.50;

    System.out.println(firstName); // Folau
    System.out.println(age);       // 30
    System.out.println(salary);    // 85000.5
}



Two Categories of Data Types

Java data types fall into two broad categories:

  1. Primitive Data Types – These are the basic building blocks provided by the language itself. There are exactly 8 primitive types: byte, short, int, long, float, double, boolean, and char. They hold their values directly in memory and are not objects.
  2. Non-Primitive (Reference) Data Types – These are types created from classes. They hold a reference (memory address) to an object rather than the value itself. Examples include String, Arrays, List, and any class you define.

Understanding the difference is fundamental to writing correct, efficient Java code. Let us start with the primitives.



Primitive Data Types

Java has exactly 8 primitive data types. Each has a fixed size in memory and a defined range of values it can hold. Here is a summary:

Type Size Range Default Value When to Use
byte 1 byte (8 bits) -128 to 127 0 Raw binary data, file I/O, saving memory in large arrays
short 2 bytes (16 bits) -32,768 to 32,767 0 Rarely used; memory-sensitive applications with small numbers
int 4 bytes (32 bits) -2,147,483,648 to 2,147,483,647 0 Default choice for whole numbers, loop counters, general arithmetic
long 8 bytes (64 bits) -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 0L Timestamps, database IDs, values exceeding int range
float 4 bytes (32 bits) ±3.4 × 1038 (6-7 decimal digits of precision) 0.0f Graphics, game engines, when memory matters more than precision
double 8 bytes (64 bits) ±1.7 × 10308 (15 decimal digits of precision) 0.0d Default for decimal numbers, scientific calculations, currency (with caution)
boolean 1 bit* true or false false Flags, conditions, on/off states
char 2 bytes (16 bits) 0 to 65,535 (Unicode characters) ‘\u0000’ Single characters, Unicode processing

* The JVM does not define a precise size for boolean. It typically uses 1 byte internally, but the logical size is a single bit.



Integer Types: byte, short, int, long

These four types store whole numbers (no decimal point). The only difference between them is how much memory they use and the range of values they can hold.

byte

The byte type is the smallest integer type. It is commonly used when reading raw binary data from files or network streams, or when you need to save memory in large arrays.

public static void main(String[] args) {
    byte minByte = -128;
    byte maxByte = 127;
    byte userAge = 25;  // Age fits easily in a byte

    System.out.println("Min byte: " + minByte);  // -128
    System.out.println("Max byte: " + maxByte);   // 127
    System.out.println("Age: " + userAge);         // 25

    // Useful for binary data
    byte[] fileData = {72, 101, 108, 108, 111};  // "Hello" in ASCII
    System.out.println(new String(fileData));      // Hello
}

short

The short type is rarely used in modern Java. It can hold values from -32,768 to 32,767. You might encounter it in legacy code or specialized applications where memory savings on large datasets matter.

public static void main(String[] args) {
    short temperature = -15;
    short elevation = 8849;   // Mount Everest height in meters
    short year = 2026;

    System.out.println("Temperature: " + temperature + "°C");  // -15°C
    System.out.println("Elevation: " + elevation + "m");        // 8849m
    System.out.println("Year: " + year);                        // 2026
}

int

The int type is the default choice for whole numbers in Java. Unless you have a specific reason to use another type, use int. It can hold values up to approximately 2.1 billion, which is sufficient for most use cases like loop counters, array indices, quantities, and general arithmetic.

public static void main(String[] args) {
    int population = 331000000;    // US population
    int salary = 95000;
    int itemCount = 0;

    // Common use: loop counter
    for (int i = 0; i < 5; i++) {
        itemCount++;
    }

    System.out.println("Population: " + population);  // 331000000
    System.out.println("Salary: $" + salary);          // $95000
    System.out.println("Items: " + itemCount);         // 5

    // You can use underscores for readability (Java 7+)
    int billion = 1_000_000_000;
    System.out.println("Billion: " + billion);         // 1000000000
}

long

The long type is used when int is not large enough. Common scenarios include timestamps (milliseconds since epoch), database primary key IDs, file sizes in bytes, and large financial calculations. Long literals must end with an L suffix.

public static void main(String[] args) {
    long worldPopulation = 8_000_000_000L;  // 8 billion - too large for int!
    long timestamp = System.currentTimeMillis();
    long fileSize = 5_368_709_120L;  // 5 GB in bytes
    long databaseId = 9_876_543_210L;

    System.out.println("World population: " + worldPopulation);
    System.out.println("Current timestamp: " + timestamp);
    System.out.println("File size: " + fileSize + " bytes");
    System.out.println("DB ID: " + databaseId);

    // Without the L suffix, Java treats the number as an int and you get an error
    // long wrong = 9876543210;  // COMPILE ERROR: integer number too large
    long correct = 9876543210L;  // Works because of the L suffix
}



Floating-Point Types: float and double

These types store decimal (fractional) numbers. The key difference between them is precision — how many decimal digits they can accurately represent.

float

The float type provides about 6-7 digits of precision. Float literals must end with an f suffix. It is used in graphics programming, game engines, and situations where memory is more important than precision.

public static void main(String[] args) {
    float pi = 3.14159f;          // Note the 'f' suffix
    float temperature = 98.6f;
    float gpa = 3.85f;
    float price = 19.99f;

    System.out.println("Pi: " + pi);              // 3.14159
    System.out.println("Temp: " + temperature);    // 98.6
    System.out.println("GPA: " + gpa);             // 3.85
    System.out.println("Price: $" + price);        // $19.99

    // Without 'f', Java treats decimal numbers as double and you get an error
    // float wrong = 3.14;  // COMPILE ERROR: incompatible types
    float correct = 3.14f;  // Works because of the f suffix
}

double

The double type is the default choice for decimal numbers in Java. It provides about 15 digits of precision, which is more than enough for most applications. When you write a decimal literal like 3.14, Java automatically treats it as a double.

public static void main(String[] args) {
    double pi = 3.141592653589793;
    double accountBalance = 1_250_000.75;
    double interestRate = 0.045;   // 4.5%

    System.out.println("Pi: " + pi);                  // 3.141592653589793
    System.out.println("Balance: $" + accountBalance); // $1250000.75

    // Calculate interest
    double interest = accountBalance * interestRate;
    System.out.println("Interest: $" + interest);      // $56250.03375

    // Precision difference between float and double
    float  floatVal  = 1.123456789f;
    double doubleVal = 1.123456789;

    System.out.println("float:  " + floatVal);   // 1.1234568  (lost precision after 7 digits)
    System.out.println("double: " + doubleVal);   // 1.123456789 (accurate to 15 digits)
}



boolean

The boolean type can hold only two values: true or false. It is used for flags, conditions, and logical expressions. Booleans are the backbone of control flow in Java — every if statement, while loop, and conditional expression evaluates to a boolean.

public static void main(String[] args) {
    boolean isLoggedIn = true;
    boolean isAdmin = false;
    boolean hasPermission = true;

    // Used in conditions
    if (isLoggedIn && hasPermission) {
        System.out.println("Access granted");   // This prints
    }

    if (isAdmin) {
        System.out.println("Welcome, admin");
    } else {
        System.out.println("Welcome, user");    // This prints
    }

    // Boolean from comparisons
    int age = 21;
    boolean canVote = age >= 18;
    boolean isTeenager = age >= 13 && age <= 19;

    System.out.println("Can vote: " + canVote);         // true
    System.out.println("Is teenager: " + isTeenager);    // false

    // Boolean with methods
    String email = "user@example.com";
    boolean validEmail = email.contains("@");
    System.out.println("Valid email: " + validEmail);    // true
}



char

The char type stores a single character. In Java, characters use Unicode (UTF-16) encoding, which means char can represent not just English letters but characters from virtually any language, as well as symbols and emojis. A char value is enclosed in single quotes ('A'), unlike strings which use double quotes ("A").

public static void main(String[] args) {
    char letter = 'A';
    char digit = '7';
    char symbol = '$';
    char space = ' ';

    System.out.println("Letter: " + letter);   // A
    System.out.println("Digit: " + digit);      // 7
    System.out.println("Symbol: " + symbol);    // $

    // char is actually a number (Unicode code point)
    char letterA = 65;       // 65 is the Unicode value for 'A'
    System.out.println(letterA);   // A

    // Unicode escape sequences
    char copyright = '\u00A9';   // copyright symbol
    char omega = '\u03A9';       // Greek capital letter Omega
    System.out.println("Copyright: " + copyright);   // ©
    System.out.println("Omega: " + omega);            // Ω

    // You can do arithmetic with char (it is a numeric type)
    char ch = 'A';
    ch++;
    System.out.println(ch);   // B

    // Convert char to its numeric value
    char c = 'Z';
    int asciiValue = (int) c;
    System.out.println("ASCII value of 'Z': " + asciiValue);  // 90
}



Non-Primitive (Reference) Data Types

Non-primitive data types are also called reference types because a variable of this type does not hold the value directly — it holds a reference (a pointer) to an object in memory. Reference types are created from classes and include String, arrays, collections, and any custom class you define.

String

String is the most commonly used reference type. Although it is not a primitive, it is so fundamental that Java gives it special treatment (you can create a String without using the new keyword). We cover Strings in detail in the Java String tutorial.

public static void main(String[] args) {
    // String is a reference type, but you can create it like a primitive
    String name = "Folau";
    String greeting = "Hello, " + name + "!";

    System.out.println(greeting);             // Hello, Folau!
    System.out.println(name.length());        // 5
    System.out.println(name.toUpperCase());   // FOLAU

    // Strings are objects with methods - primitives do not have methods
    boolean startsWithF = name.startsWith("F");
    System.out.println(startsWithF);  // true
}

Arrays

Arrays are reference types that hold a fixed-size collection of elements of the same type. We cover arrays in detail in the Java Arrays tutorial.

public static void main(String[] args) {
    // Array of integers
    int[] scores = {95, 87, 73, 100, 88};

    // Array of strings
    String[] names = {"Alice", "Bob", "Charlie"};

    System.out.println("First score: " + scores[0]);     // 95
    System.out.println("Second name: " + names[1]);      // Bob
    System.out.println("Array length: " + scores.length); // 5
}

Classes and Objects

Any class you define creates a new reference type. When you create an object using the new keyword, you get a reference to that object in memory.

class User {
    String name;
    int age;

    User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    void greet() {
        System.out.println("Hi, I'm " + name + " and I'm " + age + " years old.");
    }
}

public static void main(String[] args) {
    // 'user' is a reference variable - it holds the memory address of a User object
    User user = new User("Folau", 30);
    user.greet();  // Hi, I'm Folau and I'm 30 years old.

    // Reference types can be null - primitives cannot
    User emptyUser = null;
    System.out.println(emptyUser);  // null
}



Key Differences: Primitive vs Non-Primitive

Feature Primitive Types Non-Primitive (Reference) Types
Defined by Built into the Java language Created by the programmer (except String)
Naming Start with lowercase: int, double, boolean Start with uppercase: String, User, List
Stores The actual value directly A reference (memory address) to the object
Can be null? No — always has a value Yes — can be null
Has methods? No Yes — e.g., name.length(), user.greet()
Memory Stored on the stack (fast access) Object stored on the heap, reference on the stack
Default value 0, 0.0, false, or '\u0000' null



Type Casting

Type casting is the process of converting a value from one data type to another. In Java, there are two types of casting:

Widening Casting (Automatic)

Widening casting happens automatically when you assign a smaller type to a larger type. There is no risk of data loss because the larger type can hold all possible values of the smaller type.

The widening order is:

byteshortintlongfloatdouble

public static void main(String[] args) {
    // Widening: Java automatically converts to the larger type

    byte myByte = 42;
    short myShort = myByte;     // byte -> short (automatic)
    int myInt = myShort;        // short -> int (automatic)
    long myLong = myInt;        // int -> long (automatic)
    float myFloat = myLong;    // long -> float (automatic)
    double myDouble = myFloat; // float -> double (automatic)

    System.out.println("byte:   " + myByte);    // 42
    System.out.println("short:  " + myShort);   // 42
    System.out.println("int:    " + myInt);      // 42
    System.out.println("long:   " + myLong);     // 42
    System.out.println("float:  " + myFloat);    // 42.0
    System.out.println("double: " + myDouble);   // 42.0

    // This is also widening (int to double)
    int score = 85;
    double percentage = score;   // Automatically converts to 85.0
    System.out.println("Percentage: " + percentage);  // 85.0
}

Narrowing Casting (Manual)

Narrowing casting must be done manually by placing the target type in parentheses before the value. This is required when converting a larger type to a smaller type because there is a risk of data loss — the value might not fit in the smaller type.

The narrowing order is:

doublefloatlongintshortbyte

public static void main(String[] args) {
    // Narrowing: you must explicitly cast with (type)

    double myDouble = 9.78;
    int myInt = (int) myDouble;  // Truncates the decimal, does NOT round
    System.out.println("double: " + myDouble);  // 9.78
    System.out.println("int:    " + myInt);      // 9 (decimal part lost!)

    // More examples
    int largeNumber = 130;
    byte smallByte = (byte) largeNumber;
    System.out.println("int: " + largeNumber);       // 130
    System.out.println("byte: " + smallByte);        // -126 (overflow! 130 exceeds byte range)

    double price = 49.99;
    float floatPrice = (float) price;     // double -> float
    long longPrice = (long) price;        // double -> long (truncates decimal)
    int intPrice = (int) price;           // double -> int (truncates decimal)

    System.out.println("double: " + price);         // 49.99
    System.out.println("float:  " + floatPrice);    // 49.99
    System.out.println("long:   " + longPrice);     // 49
    System.out.println("int:    " + intPrice);       // 49

    // Without the cast, you get a compile error
    // int x = 3.14;            // COMPILE ERROR
    int x = (int) 3.14;         // OK - x is 3
}



Wrapper Classes

Every primitive type has a corresponding wrapper class that wraps the primitive value in an object. Wrapper classes are part of the java.lang package and are essential when you need to use primitives as objects — for example, in collections like ArrayList, which cannot hold primitive types directly.

Primitive Type Wrapper Class
byte Byte
short Short
int Integer
long Long
float Float
double Double
boolean Boolean
char Character

Wrapper classes also provide useful utility methods for parsing, converting, and working with their respective types.

public static void main(String[] args) {
    // Wrapper classes provide useful utility methods

    // Parsing strings to numbers
    int num = Integer.parseInt("42");
    double price = Double.parseDouble("19.99");
    boolean flag = Boolean.parseBoolean("true");

    System.out.println(num);    // 42
    System.out.println(price);  // 19.99
    System.out.println(flag);   // true

    // Getting min and max values
    System.out.println("int max: " + Integer.MAX_VALUE);     // 2147483647
    System.out.println("int min: " + Integer.MIN_VALUE);     // -2147483648
    System.out.println("double max: " + Double.MAX_VALUE);   // 1.7976931348623157E308

    // Converting numbers to strings
    String numStr = Integer.toString(42);
    String hexStr = Integer.toHexString(255);   // "ff"
    String binStr = Integer.toBinaryString(10); // "1010"

    System.out.println("Hex: " + hexStr);    // ff
    System.out.println("Binary: " + binStr); // 1010

    // Comparing values
    int result = Integer.compare(10, 20);
    System.out.println("Compare 10 vs 20: " + result);  // -1 (10 < 20)
}



Autoboxing and Unboxing

Since Java 5, the compiler can automatically convert between primitives and their wrapper classes. This feature eliminates the need for manual conversion in most cases.

  • Autoboxing: Automatic conversion from a primitive to its wrapper class (intInteger)
  • Unboxing: Automatic conversion from a wrapper class to its primitive (Integerint)
import java.util.ArrayList;
import java.util.List;

public static void main(String[] args) {
    // AUTOBOXING: primitive -> wrapper (automatic)
    Integer wrappedInt = 42;         // int 42 is autoboxed to Integer
    Double wrappedDouble = 3.14;     // double 3.14 is autoboxed to Double
    Boolean wrappedBool = true;      // boolean true is autoboxed to Boolean

    System.out.println(wrappedInt);     // 42
    System.out.println(wrappedDouble);  // 3.14
    System.out.println(wrappedBool);    // true

    // UNBOXING: wrapper -> primitive (automatic)
    int primitiveInt = wrappedInt;         // Integer is unboxed to int
    double primitiveDouble = wrappedDouble; // Double is unboxed to double

    System.out.println(primitiveInt);       // 42
    System.out.println(primitiveDouble);    // 3.14

    // Autoboxing is essential for Collections
    // ArrayList cannot hold primitive 'int', but autoboxing handles it
    List numbers = new ArrayList<>();
    numbers.add(10);    // autoboxing: int -> Integer
    numbers.add(20);    // autoboxing: int -> Integer
    numbers.add(30);    // autoboxing: int -> Integer

    // Unboxing when retrieving
    int first = numbers.get(0);   // unboxing: Integer -> int
    System.out.println("First: " + first);  // 10

    // Autoboxing in arithmetic expressions
    Integer a = 10;
    Integer b = 20;
    int sum = a + b;  // Both are unboxed, added, result stays as int
    System.out.println("Sum: " + sum);  // 30
}



When to Use Which Data Type

Choosing the right data type is an important decision. Here is practical guidance for real-world scenarios:

Scenario Recommended Type Reason
Loop counters, array indices int Default choice for whole numbers; fast and sufficient range
Age, quantity, count int Simple whole numbers within normal ranges
Database IDs long IDs can exceed int range in large databases
Timestamps long Milliseconds since epoch requires long range
File sizes long Files can exceed 2 GB (int max)
Prices, financial calculations BigDecimal Avoid float/double for money — use java.math.BigDecimal for exact precision
Scientific calculations double 15 digits of precision; default for decimals
Graphics, game coordinates float Sufficient precision, half the memory of double
Yes/no flags, feature toggles boolean Only two possible values
Single characters, Unicode char One character at a time
Raw binary data, byte streams byte File I/O and network data operate on bytes
Text, names, messages String Reference type for sequences of characters
Collections (ArrayList, HashMap) Wrapper classes Collections require objects, not primitives

Rule of thumb: Use int for whole numbers and double for decimals unless you have a specific reason to choose another type. When in doubt, go bigger — the memory savings from using byte or short are rarely worth the complexity.



Common Mistakes to Avoid

Even experienced developers make these mistakes. Understanding them early will save you hours of debugging.

1. Integer Overflow

When a value exceeds the maximum (or minimum) range of its type, it "wraps around" to the other end. Java does not throw an error — it silently overflows, which can produce very unexpected results.

public static void main(String[] args) {
    // Integer overflow - the value wraps around silently!
    int maxInt = Integer.MAX_VALUE;  // 2,147,483,647
    int overflow = maxInt + 1;

    System.out.println("Max int:     " + maxInt);     // 2147483647
    System.out.println("Max int + 1: " + overflow);   // -2147483648 (wrapped to minimum!)

    // This is a common bug when calculating large values
    int million = 1_000_000;
    int result = million * million;  // 1,000,000,000,000 - too big for int!
    System.out.println("Million squared (int):  " + result);   // -727379968 (wrong!)

    // Fix: use long
    long correctResult = (long) million * million;
    System.out.println("Million squared (long): " + correctResult);  // 1000000000000
}

2. Floating-Point Precision Errors

Floating-point numbers (float and double) cannot represent all decimal values exactly. This is a fundamental limitation of how computers store decimal numbers in binary. Never use float or double for financial calculations.

import java.math.BigDecimal;

public static void main(String[] args) {
    // Floating-point precision problem
    double a = 0.1;
    double b = 0.2;
    double sum = a + b;

    System.out.println("0.1 + 0.2 = " + sum);           // 0.30000000000000004 (not 0.3!)
    System.out.println("0.1 + 0.2 == 0.3? " + (sum == 0.3));  // false!

    // This is why you should NEVER use double for money
    double price = 0.10;
    double quantity = 3;
    double total = price * quantity;
    System.out.println("$0.10 x 3 = $" + total);  // $0.30000000000000004

    // Use BigDecimal for financial calculations
    BigDecimal bdPrice = new BigDecimal("0.10");
    BigDecimal bdQuantity = new BigDecimal("3");
    BigDecimal bdTotal = bdPrice.multiply(bdQuantity);
    System.out.println("BigDecimal: $0.10 x 3 = $" + bdTotal);  // $0.30
}

3. Comparing Wrapper Objects with ==

Using == on wrapper objects compares references, not values. This can produce surprising results because Java caches Integer values between -128 and 127.

public static void main(String[] args) {
    // Cached range (-128 to 127): == works because Java reuses the same objects
    Integer x = 100;
    Integer y = 100;
    System.out.println(x == y);       // true (cached, same object)
    System.out.println(x.equals(y));  // true

    // Outside cached range: == fails because they are different objects
    Integer a = 200;
    Integer b = 200;
    System.out.println(a == b);       // false! (different objects in memory)
    System.out.println(a.equals(b));  // true  (correct way to compare)

    // ALWAYS use .equals() to compare wrapper objects
}

4. NullPointerException with Unboxing

If a wrapper object is null and you try to unbox it to a primitive, Java throws a NullPointerException.

public static void main(String[] args) {
    Integer wrappedValue = null;

    // This throws NullPointerException at runtime!
    // int primitiveValue = wrappedValue;  // unboxing null -> crash

    // Safe approach: check for null first
    if (wrappedValue != null) {
        int safeValue = wrappedValue;
        System.out.println(safeValue);
    } else {
        System.out.println("Value is null, using default: 0");
        int safeValue = 0;
    }
}

5. Forgetting Suffixes for long and float

Long literals need an L suffix, and float literals need an f suffix. Without them, Java treats the value as int or double, which can cause compile errors or unexpected behavior.

public static void main(String[] args) {
    // WRONG: no suffix
    // long bigNumber = 3000000000;   // COMPILE ERROR: integer too large
    // float pi = 3.14;               // COMPILE ERROR: incompatible types

    // CORRECT: with suffix
    long bigNumber = 3000000000L;     // L suffix for long
    float pi = 3.14f;                 // f suffix for float

    System.out.println(bigNumber);    // 3000000000
    System.out.println(pi);           // 3.14

    // Tip: Use uppercase L to avoid confusion with the digit 1
    long value = 123456789L;  // Clear
    // long value = 123456789l;  // Confusing - lowercase l looks like 1
}



Summary

  • Java has 8 primitive types: byte, short, int, long, float, double, boolean, and char.
  • Non-primitive (reference) types include String, arrays, and any class you define. They hold references to objects, not the values themselves.
  • Use int as your default for whole numbers and double as your default for decimals.
  • Widening casting (small to large) happens automatically; narrowing casting (large to small) must be explicit and risks data loss.
  • Every primitive has a wrapper class (Integer, Double, etc.) for use in collections and APIs that require objects.
  • Autoboxing/unboxing converts between primitives and wrappers automatically, but beware of null values causing NullPointerException.
  • Watch out for integer overflow, floating-point precision issues, and reference comparison pitfalls with wrapper classes.

In the next tutorial, we will cover Java Operators, where you will learn how to perform arithmetic, comparisons, and logical operations on these data types.

Previous: Java Variables | Next: Java Operators

March 8, 2019

Java Variables

What Is a Variable?

Think of a variable as a labeled box in your computer’s memory. You give the box a name, decide what kind of thing it can hold, and then put a value inside it. Later, you can look at the value, change it, or pass it along to another part of your program.

More formally, a variable is a named memory location that stores a value of a specific data type. Every Java program you write will be full of variables, so understanding them deeply is one of the most important steps in becoming a strong developer.

// A simple variable: a labeled box named "age" that holds the integer 30
int age = 30;
System.out.println(age); // Output: 30

Declaring and Initializing Variables

In Java, creating a variable is a two-part process: declaration and initialization. You can do them separately or on the same line.

Declaration tells the compiler: “Reserve a spot in memory with this name and this type.”

Initialization tells the compiler: “Put this value in that spot.”

The general syntax is:

type name = value;

// Declaration and initialization on the same line
String firstName = "Folau";
int numberOfItems = 42;
double accountBalance = 1250.75;
boolean isActive = true;

// Declaration first, initialization later
String lastName;
lastName = "Kaveinga";

// Declaring multiple variables of the same type on one line
int x = 1, y = 2, z = 3;

System.out.println(firstName);      // Output: Folau
System.out.println(lastName);       // Output: Kaveinga
System.out.println(numberOfItems);  // Output: 42
System.out.println(accountBalance); // Output: 1250.75
System.out.println(isActive);       // Output: true
System.out.println(x + y + z);     // Output: 6

A variable has three essential parts:

1. Type – What kind of data it holds (int, String, double, boolean, etc.)

2. Name – The label you use to refer to it in your code

3. Value – The actual data stored in memory

The equals sign (=) is the assignment operator. It takes the value on the right and stores it in the variable on the left. This is not the same as mathematical equality. It means “assign this value to this variable.”

// The assignment operator in action
String firstName = "Folau";
System.out.println(firstName); // Output: Folau

// Reassigning a new value to the same variable
firstName = "Lisa";
System.out.println(firstName); // Output: Lisa

// The old value "Folau" is gone. The box now holds "Lisa".

Types of Variables in Java

Java has three categories of variables based on where they are declared. Understanding the differences is critical because it affects their lifetime, default values, and visibility.

1. Local Variables

Local variables are declared inside a method, constructor, or block. They exist only while that method or block is executing, and they are destroyed once execution leaves that scope.

Key facts about local variables:

– They must be initialized before use. Java will not assign a default value. If you try to use an uninitialized local variable, the compiler will throw an error.

– They are not accessible outside the method or block where they are declared.

– They cannot have access modifiers (public, private, protected) because they belong to the method, not the class.

public class LocalVariableExample {

    public void calculateTotal() {
        // These are local variables - they exist only inside this method
        int quantity = 5;
        double pricePerItem = 19.99;
        double total = quantity * pricePerItem;

        System.out.println("Total: $" + total); // Output: Total: $99.95
    }

    public void anotherMethod() {
        // This will NOT compile - "total" does not exist here
        // System.out.println(total); // ERROR: cannot find symbol

        // You must declare your own variables in this scope
        double total = 0.0;
        System.out.println("Total: $" + total); // Output: Total: $0.0
    }

    public static void main(String[] args) {
        LocalVariableExample example = new LocalVariableExample();
        example.calculateTotal();
        example.anotherMethod();
    }
}

2. Instance Variables (Non-Static Fields)

Instance variables are declared inside a class but outside any method. Each object (instance) of the class gets its own copy of these variables. If you create three Person objects, each one has its own separate name and age.

Key facts about instance variables:

– They receive default values if you do not initialize them (0 for numbers, false for booleans, null for objects).

– They exist as long as the object exists.

– They can have access modifiers (private, public, protected).

public class Person {

    // Instance variables - each Person object gets its own copy
    private String name;
    private int age;
    private String email;

    public Person(String name, int age, String email) {
        this.name = name;
        this.age = age;
        this.email = email;
    }

    public void introduce() {
        System.out.println("Hi, I'm " + name + ", age " + age);
    }

    public static void main(String[] args) {
        // Two separate objects, each with their own instance variables
        Person person1 = new Person("Folau", 30, "folau@example.com");
        Person person2 = new Person("Lisa", 28, "lisa@example.com");

        person1.introduce(); // Output: Hi, I'm Folau, age 30
        person2.introduce(); // Output: Hi, I'm Lisa, age 28

        // Changing person1's name does NOT affect person2
        // Each object owns its own copy of the instance variables
    }
}

3. Static Variables (Class Variables)

Static variables are declared with the static keyword inside a class but outside any method. Unlike instance variables, there is only one copy shared across all instances of the class. If one object changes a static variable, every other object sees the change.

Static variables are perfect for values that should be the same for every instance, such as a counter that tracks how many objects have been created, or a configuration value shared by the whole application.

public class Employee {

    // Static variable - shared by ALL Employee objects
    private static int employeeCount = 0;
    private static String companyName = "LoveMeSomeCoding";

    // Instance variables - each Employee gets its own copy
    private String name;
    private int id;

    public Employee(String name) {
        this.name = name;
        employeeCount++;         // Increment the shared counter
        this.id = employeeCount; // Assign a unique ID
    }

    public void displayInfo() {
        System.out.println("ID: " + id + ", Name: " + name + ", Company: " + companyName);
    }

    public static int getEmployeeCount() {
        return employeeCount;
    }

    public static void main(String[] args) {
        Employee emp1 = new Employee("Folau");
        Employee emp2 = new Employee("Lisa");
        Employee emp3 = new Employee("James");

        emp1.displayInfo(); // Output: ID: 1, Name: Folau, Company: LoveMeSomeCoding
        emp2.displayInfo(); // Output: ID: 2, Name: Lisa, Company: LoveMeSomeCoding
        emp3.displayInfo(); // Output: ID: 3, Name: James, Company: LoveMeSomeCoding

        // The static variable is shared - all three objects see the same count
        System.out.println("Total employees: " + Employee.getEmployeeCount());
        // Output: Total employees: 3
    }
}

Variable Scope

Scope defines where a variable can be accessed in your code. Once you leave a variable’s scope, it no longer exists. Java has three levels of scope you need to understand.

Block Scope

A variable declared inside a pair of curly braces { } only exists within those braces. This includes if-statements, for-loops, while-loops, and any other block.

Method Scope

A variable declared at the top of a method is accessible anywhere within that method, but not outside of it. Method parameters also have method scope.

Class Scope

Instance and static variables have class scope. They are accessible from any method within the class (and potentially from outside the class, depending on their access modifier).

public class ScopeDemo {

    // Class scope - accessible from any method in this class
    private String classLevelVar = "I'm available everywhere in this class";

    public void demonstrateScope() {
        // Method scope - accessible anywhere in this method
        int methodLevelVar = 100;

        System.out.println(classLevelVar);  // OK - class scope
        System.out.println(methodLevelVar); // OK - method scope

        if (methodLevelVar > 50) {
            // Block scope - only exists inside this if-block
            String blockLevelVar = "I only exist inside this if-block";
            System.out.println(blockLevelVar);  // OK - we are inside the block
            System.out.println(methodLevelVar); // OK - method scope is wider
        }

        // This will NOT compile - blockLevelVar is out of scope
        // System.out.println(blockLevelVar); // ERROR: cannot find symbol

        for (int i = 0; i < 3; i++) {
            // "i" only exists inside this for-loop
            System.out.println("Loop index: " + i);
        }

        // This will NOT compile - "i" is out of scope
        // System.out.println(i); // ERROR: cannot find symbol
    }

    public void anotherMethod() {
        System.out.println(classLevelVar); // OK - class scope
        // System.out.println(methodLevelVar); // ERROR - method scope is limited to demonstrateScope()
    }

    public static void main(String[] args) {
        ScopeDemo demo = new ScopeDemo();
        demo.demonstrateScope();
    }
}

The var Keyword (Java 10+)

Starting with Java 10, you can use var to let the compiler figure out the type for you. This is called local variable type inference. The compiler looks at the value on the right side of the assignment and determines the type automatically.

Important rules about var:

- var only works with local variables. You cannot use it for instance variables, static variables, or method parameters.

- The variable must be initialized on the same line. The compiler needs the right-hand side to infer the type.

- var is not a keyword technically - it is a reserved type name, so you could still name a variable "var" (though you should not).

- The type is still determined at compile time. Java is still statically typed. var is just syntactic convenience, not dynamic typing.

public class VarExample {

    public static void main(String[] args) {
        // Without var - explicit types
        String name = "Folau";
        int age = 30;
        List hobbies = new ArrayList<>();

        // With var - the compiler infers the types
        var name2 = "Lisa";                    // Inferred as String
        var age2 = 28;                         // Inferred as int
        var hobbies2 = new ArrayList(); // Inferred as ArrayList

        System.out.println(name2.getClass().getSimpleName());    // Output: String
        System.out.println(((Object) age2).getClass().getSimpleName()); // Output: Integer

        // var is great for reducing verbosity with long type names
        var employeeMap = new HashMap>();

        // Without var, that same line would be:
        // HashMap> employeeMap = new HashMap>();

        // These will NOT compile:
        // var x;            // ERROR: cannot infer type - no initializer
        // var y = null;     // ERROR: cannot infer type - null has no type
    }
}

A good rule of thumb: use var when the type is obvious from the right-hand side of the assignment. If it makes the code harder to read, stick with the explicit type.

Constants with the final Keyword

If you have a value that should never change after it is set, mark it with the final keyword. Once assigned, a final variable cannot be reassigned. This is how you create constants in Java.

By convention, constants are named using UPPER_SNAKE_CASE (all uppercase letters with underscores between words). When combined with the static keyword, a final variable becomes a true class-level constant shared across all instances.

public class ConstantExample {

    // Class-level constants: static + final
    // Convention: UPPER_SNAKE_CASE
    public static final double PI = 3.14159265358979;
    public static final int MAX_LOGIN_ATTEMPTS = 5;
    public static final String DEFAULT_CURRENCY = "USD";

    // Instance-level final variable - set once per object, then locked
    private final String createdAt;

    public ConstantExample() {
        this.createdAt = java.time.LocalDateTime.now().toString();
        // createdAt cannot be changed after this point
    }

    public void tryToBreakConstants() {
        // This will NOT compile - cannot reassign a final variable
        // PI = 3.14; // ERROR: cannot assign a value to final variable PI

        // Local final variable
        final int maxRetries = 3;
        // maxRetries = 5; // ERROR: cannot assign a value to final variable maxRetries

        System.out.println("PI: " + PI);
        System.out.println("Max login attempts: " + MAX_LOGIN_ATTEMPTS);
        System.out.println("Created at: " + createdAt);
    }

    public static void main(String[] args) {
        ConstantExample example = new ConstantExample();
        example.tryToBreakConstants();
        // Output:
        // PI: 3.14159265358979
        // Max login attempts: 5
        // Created at: 2026-02-28T10:30:00.123456
    }
}

Default Values for Instance and Static Variables

When you declare an instance or static variable without assigning a value, Java automatically gives it a default value. This does not apply to local variables. Local variables have no default and must be initialized before use.

Here is the full table of default values:

Data Type Default Value
byte 0
short 0
int 0
long 0L
float 0.0f
double 0.0
char '\u0000' (null character)
boolean false
Any object (String, arrays, etc.) null
public class DefaultValueDemo {

    // Instance variables with no explicit initialization
    int number;
    double decimal;
    boolean flag;
    char character;
    String text;
    int[] numbers;

    public void printDefaults() {
        System.out.println("int default:     " + number);    // Output: 0
        System.out.println("double default:  " + decimal);   // Output: 0.0
        System.out.println("boolean default: " + flag);      // Output: false
        System.out.println("char default:    [" + character + "]"); // Output: [ ] (null character)
        System.out.println("String default:  " + text);      // Output: null
        System.out.println("Array default:   " + numbers);   // Output: null
    }

    public void localVariableExample() {
        int localNumber;
        // This will NOT compile - local variables have no default value
        // System.out.println(localNumber); // ERROR: variable localNumber might not have been initialized

        // You must assign a value first
        localNumber = 10;
        System.out.println(localNumber); // Output: 10
    }

    public static void main(String[] args) {
        DefaultValueDemo demo = new DefaultValueDemo();
        demo.printDefaults();
        demo.localVariableExample();
    }
}

Variable Naming Rules

Java enforces specific rules about what constitutes a valid variable name. If you break these rules, your code will not compile.

Rule 1: Variable names can contain letters, digits, underscores (_), and dollar signs ($).

// Valid names using allowed characters
int age = 25;
int _count = 10;
int $price = 99;
int item2 = 5;

// Invalid - contains special characters that are not allowed
// int my-variable = 1;  // ERROR: '-' is not allowed
// int my variable = 1;  // ERROR: spaces are not allowed
// int my@name = 1;      // ERROR: '@' is not allowed

Rule 2: Variable names must begin with a letter, underscore, or dollar sign. They cannot start with a digit.

// Valid - starts with a letter, underscore, or dollar sign
int age = 25;
int _hidden = 0;
int $amount = 100;

// Invalid - starts with a digit
// int 10things = 10;   // ERROR: cannot start with a digit
// int 1stPlace = 1;    // ERROR: cannot start with a digit

Rule 3: Variable names are case-sensitive. The variables firstName and firstname are two completely different variables.

String firstName = "Folau"; // 'N' is uppercase
String firstname = "Lisa";  // 'n' is lowercase

// These are two separate variables
System.out.println(firstName); // Output: Folau
System.out.println(firstname); // Output: Lisa

Rule 4: Variable names cannot be Java reserved keywords. Words like int, double, class, static, public, return, and about 50 others are reserved for the language itself.

// Invalid - these are reserved keywords
// int class = 5;      // ERROR: 'class' is a keyword
// int static = 10;    // ERROR: 'static' is a keyword
// int return = 0;     // ERROR: 'return' is a keyword
// String double = ""; // ERROR: 'double' is a keyword

// Valid - similar but not identical to keywords
int classCount = 5;
int staticValue = 10;
String returnMessage = "success";

Rule 5: Variable names cannot contain whitespace.

// Invalid - contains spaces
// String first name = "Folau"; // ERROR: spaces not allowed

// Valid alternatives
String firstName = "Folau";   // camelCase (preferred in Java)
String first_name = "Folau";  // snake_case (works but not standard Java style)

Variable Naming Conventions

Beyond the rules the compiler enforces, the Java community follows strong naming conventions. These are not required for your code to compile, but following them makes your code professional and readable. In a team environment, consistent naming is expected.

1. Use camelCase for Variables and Methods

In Java, the standard convention is camelCase: start with a lowercase letter, and capitalize the first letter of each subsequent word. This is different from Python and C, which commonly use snake_case.

// Correct - camelCase (Java convention)
String firstName = "Folau";
int numberOfStudents = 42;
double accountBalance = 1500.00;
boolean isLoggedIn = true;

// Avoid - snake_case is not standard Java style
// String first_name = "Folau";
// int number_of_students = 42;

2. Use UPPER_SNAKE_CASE for Constants

Variables declared with static final should use all uppercase letters with underscores separating words.

// Correct - UPPER_SNAKE_CASE for constants
public static final int MAX_RETRY_COUNT = 3;
public static final String BASE_URL = "https://lovemesomecoding.com";
public static final double TAX_RATE = 0.08;

// Incorrect - inconsistent style
// public static final int maxRetryCount = 3;     // looks like a regular variable
// public static final int Max_Retry_Count = 3;   // mixed style

3. Use Descriptive, Meaningful Names

Great developers write code that reads like a story. A variable name should tell you exactly what it represents without needing a comment. Do not abbreviate unless the abbreviation is universally understood (like "id" or "url").

// Good - clear and descriptive
int numberOfStudents = 30;
double monthlyPayment = 1200.50;
String customerEmailAddress = "folau@example.com";
boolean hasActiveSubscription = true;
List<String> pendingOrderIds = new ArrayList<>();

// Bad - vague, abbreviated, or lazy
int n = 30;          // What does "n" mean?
double mp = 1200.50; // Unclear abbreviation
String cea = "";     // Unreadable
boolean flag = true; // Flag for what?
List<String> list = new ArrayList<>(); // List of what?

4. Use Prefixes for Booleans

Boolean variables should read like a yes/no question. Start them with is, has, can, should, or similar prefixes.

// Good - reads naturally as a true/false question
boolean isActive = true;
boolean hasPermission = false;
boolean canDelete = true;
boolean shouldRetry = false;
boolean isEligibleForDiscount = true;

// Bad - unclear whether it is a boolean or something else
boolean active = true;     // "active" could be a noun
boolean permission = false; // sounds like it holds a permission object
boolean delete = true;     // sounds like a verb/action

5. Be Consistent

Whatever naming style you choose, stick with it throughout your entire project. If you name one constant NUMBER_OF_PERSONS, do not name the next one Number_Of_Rocks. Consistency makes your codebase predictable and easy to navigate.

Remember: code is written for humans to read. Reading code should be like reading a story. It should flow and make sense. Your variable names are one of the strongest signals of how well you understand the program you are building.

Common Mistakes and Best Practices

After years of writing Java professionally, here are the mistakes I see most often with variables, along with the practices that strong developers follow.

Mistake 1: Using an Uninitialized Local Variable

public void processOrder() {
    double total;

    // MISTAKE: using "total" before assigning a value
    // System.out.println(total); // ERROR: variable total might not have been initialized

    // FIX: always initialize local variables
    total = 0.0;
    System.out.println(total); // Output: 0.0
}

Mistake 2: Shadowing Variables

Shadowing happens when a local variable has the same name as an instance variable. This can cause subtle bugs because you might think you are modifying the instance variable when you are actually modifying a local copy.

public class ShadowingExample {

    private String name = "Default";

    public void setName(String name) {
        // MISTAKE: this assigns the parameter to itself, not the instance variable
        // name = name; // Does nothing useful!

        // FIX: use "this" to refer to the instance variable
        this.name = name;
    }

    public static void main(String[] args) {
        ShadowingExample example = new ShadowingExample();
        example.setName("Folau");
        System.out.println(example.name); // Output: Folau
    }
}

Mistake 3: Declaring Variables Too Early or Too Broadly

Declare variables as close as possible to where they are first used, and in the narrowest scope possible. This makes your code easier to follow and reduces the chance of accidental misuse.

// Bad - declared far from where it is used
public void processOrders(List<Order> orders) {
    String status;       // Declared here...
    double discount;     // ...but used 20 lines later

    for (Order order : orders) {
        // ... lots of code ...
    }

    status = "complete";
    discount = 0.10;
}

// Good - declared right where it is needed
public void processOrders(List<Order> orders) {
    for (Order order : orders) {
        // ... lots of code ...
    }

    String status = "complete";
    double discount = 0.10;
}

Mistake 4: Using Magic Numbers Instead of Named Constants

A "magic number" is a raw numeric value in your code with no explanation. Replace them with named constants so the code is self-documenting.

// Bad - what does 5 mean? What does 0.08 mean?
if (loginAttempts > 5) {
    lockAccount();
}
double tax = subtotal * 0.08;

// Good - the constants explain themselves
public static final int MAX_LOGIN_ATTEMPTS = 5;
public static final double SALES_TAX_RATE = 0.08;

if (loginAttempts > MAX_LOGIN_ATTEMPTS) {
    lockAccount();
}
double tax = subtotal * SALES_TAX_RATE;

Best Practices Summary

1. Always initialize local variables. Do not rely on remembering to set them later.

2. Use the narrowest scope possible. If a variable is only needed inside a loop, declare it inside the loop.

3. Prefer final when a variable should not change. This communicates intent and prevents accidental reassignment.

4. Use meaningful, descriptive names. Spend an extra 10 seconds naming a variable well. It will save you and your teammates hours of confusion later.

5. Follow Java naming conventions. camelCase for variables and methods, UPPER_SNAKE_CASE for constants, PascalCase for class names.

6. Replace magic numbers with named constants. Your future self will thank you.

7. One variable, one purpose. Do not reuse a variable for a different purpose partway through a method. Create a new variable with a descriptive name instead.

Full Example: Putting It All Together

Here is a complete, runnable program that demonstrates every concept covered in this tutorial.

public class JavaVariablesTutorial {

    // ---- Static (class) variables ----
    private static int instanceCount = 0;
    public static final String APP_NAME = "LoveMeSomeCoding";
    public static final int MAX_USERS = 1000;

    // ---- Instance variables (default values if not set) ----
    private String username;
    private int loginAttempts;
    private boolean isActive;

    // Constructor
    public JavaVariablesTutorial(String username) {
        this.username = username;
        this.isActive = true;
        instanceCount++;
    }

    // Method demonstrating local variables and scope
    public void displayProfile() {
        // Local variable - must be initialized
        String greeting = "Welcome back, " + username + "!";
        System.out.println(greeting);
        System.out.println("Active: " + isActive);
        System.out.println("Login attempts: " + loginAttempts); // default value: 0

        // Block scope
        if (isActive) {
            String status = "Status: Online";
            System.out.println(status);
        }
        // "status" is no longer accessible here

        // var keyword (Java 10+)
        var totalUsers = instanceCount;
        System.out.println("Total users created: " + totalUsers);
    }

    // Method demonstrating final (local constant)
    public boolean canLogin() {
        final int maxAttempts = 5;
        return loginAttempts < maxAttempts;
    }

    public void attemptLogin() {
        if (canLogin()) {
            loginAttempts++;
            System.out.println(username + " logged in. Attempts: " + loginAttempts);
        } else {
            System.out.println(username + " is locked out!");
        }
    }

    public static void main(String[] args) {
        System.out.println("App: " + APP_NAME);
        System.out.println("Max users allowed: " + MAX_USERS);
        System.out.println("---");

        // Create instances - each has its own instance variables
        JavaVariablesTutorial user1 = new JavaVariablesTutorial("Folau");
        JavaVariablesTutorial user2 = new JavaVariablesTutorial("Lisa");

        user1.displayProfile();
        System.out.println("---");
        user2.displayProfile();
        System.out.println("---");

        // Demonstrate login attempts (instance variable tracks per user)
        user1.attemptLogin();
        user1.attemptLogin();
        user2.attemptLogin();

        System.out.println("---");
        System.out.println("Total instances created: " + instanceCount);
    }
}

/*
 * Output:
 * App: LoveMeSomeCoding
 * Max users allowed: 1000
 * ---
 * Welcome back, Folau!
 * Active: true
 * Login attempts: 0
 * Status: Online
 * Total users created: 2
 * ---
 * Welcome back, Lisa!
 * Active: true
 * Login attempts: 0
 * Status: Online
 * Total users created: 2
 * ---
 * Folau logged in. Attempts: 1
 * Folau logged in. Attempts: 2
 * Lisa logged in. Attempts: 1
 * ---
 * Total instances created: 2
 */

Summary

Variables are the foundation of every Java program. Here is what you should take away from this tutorial:

A variable is a named memory location with a type, a name, and a value.

Local variables live inside methods and must be initialized before use. Instance variables belong to objects and get default values. Static variables are shared across all instances of a class.

Scope determines where a variable is visible: block, method, or class level. Always use the narrowest scope possible.

The var keyword (Java 10+) lets the compiler infer the type of local variables from their initializer.

The final keyword makes a variable a constant that cannot be reassigned.

Follow naming conventions consistently: camelCase for variables, UPPER_SNAKE_CASE for constants. Choose clear, descriptive names that make your code read like a well-written story.

Master these fundamentals and you will have a rock-solid foundation for everything else in Java.

March 8, 2019

Notification System

A notification system has already become a very popular feature for many applications in recent years. A notification alerts a user with important information like breaking news, product updates, events, offerings, etc. It has become an indispensable part of our daily life. In this chapter, you are asked to design a notification system.

A notification is more than just mobile push notification. Three types of notification formats are: mobile push notification, SMS message, and Email. Figure 10-1 shows an example of each of these notifications.

 

 

Solution

Service 1 to N: They represent different services that send notifications via APIs provided by notification servers.

Notification servers: They provide the following functionalities:

  • Provide APIs for services to send notifications. Those APIs are only accessible internally or by verified clients to prevent spams.
  • Carry out basic validations to verify emails, phone numbers, etc.
  • Query the database or cache to fetch data needed to render a notification. • Put notification data to message queues for parallel processing.

Cache: User info, device info, notification templates are cached.

DB: It stores data about user, notification, settings, etc.

Message queues: They remove dependencies between components. Message queues serve as buffers when high volumes of notifications are to be sent out. Each notification type is assigned with a distinct message queue so an outage in one third-party service will not affect other notification types.

Workers: Workers are a list of servers that pull notification events from message queues and send them to the corresponding third-party services.

Third-party services: Already explained in the initial design.

iOS, Android, SMS, Email: Already explained in the initial design.

Now, let us examine how every component works together to send a notification:

  1. A service calls APIs provided by notification servers to send notifications.
  2. Notification servers fetch metadata such as user info, device token, and notification setting from the cache or database.
  3. A notification event is sent to the corresponding queue for processing. For instance, an iOS push notification event is sent to the iOS PN queue.
  4. Workers pull notification events from message queues. 5. Workers send notifications to third party services.
  5. Third-party services send notifications to user devices.

How to prevent data loss?

One of the most important requirements in a notification system is that it cannot lose data. Notifications can usually be delayed or re-ordered, but never lost. To satisfy this requirement, the notification system persists notification data in a database and implements a retry mechanism.

Will recipients receive a notification exactly once?

The short answer is no. Although notification is delivered exactly once most of the time, the distributed nature could result in duplicate notifications. To reduce the duplication occurrence, we introduce a dedupe mechanism and handle each failure case carefully. Here is a simple dedupe logic:

When a notification event first arrives, we check if it is seen before by checking the event ID. If it is seen before, it is discarded. Otherwise, we will send out the notification.

Notification template

A large notification system sends out millions of notifications per day, and many of these notifications follow a similar format. Notification templates are introduced to avoid building every notification from scratch. A notification template is a preformatted notification to create your unique notification by customizing parameters, styling, tracking links, etc. Here is an example template of push notifications.

BODY:
You dreamed of it. We dared it. [ITEM NAME] is back — only until [DATE]. CTA:
Order Now. Or, Save My [ITEM NAME]
The benefits of using notification templates include maintaining a consistent format, reducing the margin error, and saving time.

Notification setting

Users generally receive way too many notifications daily and they can easily feel overwhelmed. Thus, many websites and apps give users fine-grained control over notification settings. This information is stored in the notification setting table, with the following fields:

user_id bigInt
channel varchar # push notification, email or SMS 
opt_in boolean # opt-in to receive notification

Before any notification is sent to a user, we first check if a user is opted-in to receive this type of notification.

Rate limiting

To avoid overwhelming users with too many notifications, we can limit the number of notifications a user can receive. This is important because receivers could turn off notifications completely if we send too often.

Retry mechanism

When a third-party service fails to send a notification, the notification will be added to the message queue for retrying. If the problem persists, an alert will be sent out to developers.

Here is the final design, many new components are added in comparison with the previous design.

  • The notification servers are equipped with two more critical features: authentication and rate-limiting.
  • We also add a retry mechanism to handle notification failures. If the system fails to send notifications, they are put back in the messaging queue and the workers will retry for a predefined number of times.
  • Furthermore, notification templates provide a consistent and efficient notification creation process.
  • Finally, monitoring and tracking systems are added for system health checks and future improvements.

March 5, 2019

Introduction

What is system design?

System design is the process of designing the elements of a system such as the architecture, modules and components, the different interfaces of those components and the data that goes through that system. It is meant to satisfy specific needs and requirements of a business or organization through the engineering of a coherent and well-running system.

What is the purpose of system design

The purpose of the System Design process is to provide sufficient detailed data and information about the system and its system elements to enable the implementation consistent with architectural entities as defined in models and views of the system architecture.

How do you design a system?
(we are using a chat app as an example)

    • Step #1 understand the requirements or the problem. In most cases, when you given a new feature or a new system to develop, you are not given all the little details that you need to know. It is always a good idea to ask questions about the requirements. You might not have the domain knowledge to even understand the requirements. For example, you have to design tinder but you have never used a dating app before. If that is the case, ask for more information. The better you understand the requirements the more likely you will design a system that meets the expected outcome. One of the many important things you need to know(from the beginning) is the scope of the problem. You have to figure out what you are working to solve and what can be pushed until later. You also need to know how you will need to scale the system.
      Questions
      – What kind of chat app shall we design? 1 on 1 or group based?
      – Is this a mobile app? Or a web app? Or both?
      – What is the scale of this app? A startup app or massive scale?
      – For group chat, what is the group member limit?
      – Is there a message size limit?
      – How long shall we store the chat history?
      – What network bandwidth usage are we expecting? This will be crucial in deciding how we will manage traffic and balance load between servers.
    •  Step #2 propose high-level design. In this step, draw up a high-level design using wireframe or flow chart that represents the players/entities of the system. You should draw this flow chart from the requirements. Make sure you confirm with product owner that this is what he has in mind. For our example, here are the main entities we are dealing with: sender, receiver, and chat service which is what connects them. Don’t forget to focus on the main functionalities of the system and not the little details.

      You also can walk through how the system will function from a very high level(1000 ft view). 

 

  • Step #3 deep dive. In this step, you dive into APIs and endpoints. You also dive into the data model of each entity in the system.

    For endpoints
    Users: create, update
    Messages: message, timeCreated, destination, etc

    For data model
    User: firstName, lastName, displayName, email, password, etc
    Group: id, list of users

    Also talk about which programming language, framework, cloud infrastructure, etc to use.

    You also walk through a happy path of how a user uses the system.

  • Step #4  identify and resolve problems. Try to discuss as many bottlenecks as possible and different approaches to mitigate them. 

 

March 4, 2019

Max Subset Sum No Adjacent

Write a function that takes in an array of positive integers and returns the maximum sum of non-adjacent elements in the array.

If the input array is empty, the function should return 0.

Sample input

[75, 105, 120, 75, 90, 135]

Sample output

330 = 75 + 120 + 135

Solution

Time Complexity: O(n)

Space Complexity: O(1)

static int maxSubsetSumNoAdjacent(int[] array) {

    int answer = 0;

    if (array.length == 0) {
        return 0;
    } else if (array.length == 1) {
        return array[0];
    }

    int first = Math.max(array[0], array[1]);
    int second = array[0];
    int current = 0;
    

    for (int i = 2; i < array.length; i++) {
        current = Math.max(first, second + array[i]);
        second = first;
        first = current;

    }

    answer = first;

    return answer;
}

 

Source code on Github

February 16, 2019