Skip to main content

Technical Interviews

· One min read

I have been studying for technical interviews and I do not really know what to expect from these interviews. I assume there would be some coding exercises and maybe a system design question. I suck at LeetCode problems so I should probably do more of them.

Resources:

Youtubers:

System Design

· 5 min read

Bit.ly

Bit.ly is a URL shortener. This is a pretty common beginner systems question.

Steps for Designing

  1. Functional Requirements: What features must the system have to meet the needs of the user.
  • Core Requirements:
    • Users should be able to submit a long URL and receive a short one
    • Users should be able to access the original URL from the short one (Forwarding)
  1. Non-Functional Requirements: Features that refer to how the system operates and provides the functional features.
  • Short URLs should be unique
  • The redirect delay should be minimal
  • High availability (99.99% >)
  • The system should scale to 1 billion URLs and 100M DAU (Daily Active Users)

Setup

Core Entities -- What Are They?

Core entities represent the primary objects in our system. These are derived from our requirements. Examples can include "URL", "User", "Transaction" and so on. They often map directly to database tables but can also represent more abstract concepts as well.

When Are Entities Tables in a Database?

In a relatoional database design, more core entities will have corresponding tables.

  • Each table normally represents one entity (e.g, a "Users" table for the User entity) Some entities may not need direct table representation:
  • Derived or computed entities (e.g, an aggregated click count)
  • Temporary or in-memory entities used for processing only.
When Are They Not Tables?
  1. NoSQL Databases
  2. Microservices
  3. When Aggregating Data

The core entities for the Bit.ly URL shortener are:

  • Original URL: The URL from the user
  • Short URL: The shortened processes URL that is sent to the user and mapped to the original URL for forwarding
  • User: The user who created the shortened URL

API

What Is It?

The API is the contract between the client and the server. How we move data from client to server and vice versa. There are many different types of APIs, but we will use REST and the HTTP methods.

(CRUD)

  • POST: Create
  • GET: Read
  • PUT: Update
  • DELETE: Delete

Now before the APIs are built, we should consider the services offered and create a separation of concerns. There are actually two services being offered, a URL shortener and a forwarding service. One is incredibly reliant on the other.

Shortening The URL POST Endpoint

This API endpoint will take in the long URL as well as a custom alias and expiration date.

// URL POST
{
"long_url": "https://example.com/some/long/ass/path",
"alias": "short_alias",
"exp_data": "optional_expiration_data"
}
->
{
"short_url": "http://short.ly/abc"
}

Redirection

//Redirect to original URL
GET /{short_code}
-> HTTP 302 Redirect to the original URL recieved from the user.

High-Level Design

We start the design by going one-by-one through our functional requirements and designing single systems to meet them.

URL Shortener (POST)

The URL shortener core requirement should take a POST request from the user, compute a shortened URL (optional alias), and then store the record in the database.

image

  1. User: Interacts with the system via an API enpoint
  2. Server: Receives and processes the request from the client or user and handles all the logic like shortening the URL and validating it to already created URLs.
  3. Database: Stores the map of short codes to long URLs, along with the aliases and expiration dates.

When the system recieves a POST request from the user:

  1. The server recieves and validates the URL:
    • Use an opensource library to validate the the long URL
    • Queery the database to see if the long URL is already being forwarded from (record already exists)
  2. If the URL is valid and is not already in our database we generate a short URL and store in our database:
  3. Finally, we can return the short URL to our user.

Acess Original URL Via Short URL (GET)

Users should be able to access the original URL from the shortened URL.

image

When the system recieves a GET request from the user with a shortened URL:

  1. The server will lookup the short URL and verify that there is a match and it has not expired.
  2. If the URL is valid and has not expired, the server will respond with a 302 redirect what will point to the original long URL.

Some Scalability and Deep Dives

URL Uniqueness

I would imaging that using a hashing function would work. Adding a hash feature to the URL entity could make chaining possible. Especially if we use SHA-256 for optimal number of hashes.

Column NameData Type
URLVARCHAR(2048)
shortURLVARCHAR(255)
hashVARCHAR(64)
createdTIMESTAMP
expirationTIMESTAMP
createdByVARCHAR(255)

The next entry would have to read the previous entry's hash then incoporate that into computing its hash. This would create a chain. We could also add an authentiction server that stores hashes in a hashmap that can be quickly searched for verification purposes.

Scale to 1B Shortened URLs and 100M DAU

Scaling can be done simply. We can have a separation of concerns from the URL shortener service and the forwarding service. We can assume that less links will be made than they will be searched since 1 user can shorten a link and any one can use it to get to the original URL.

Scaling horizontally will make it easier if we separate our services out to different servers and architecture.

image

Exploring Go's Type System vs Rust

· 7 min read

I have been hearing a lot of hoopla about Rust's type system and how it is better than Go's from a bunch of crusty rustaceans. I realized that although I have used Go for some projects and really enjoy the language, I do not understand the type system as well as Python's and C's. I would like to do a deep dive, and compare Rust to Go from a beginner's perspective.

Primitive Type

Scalar Types

Integers

Rust

  • i8, i16, i32, i64, i128: Signed ints for different bit sizes
  • u...: the same thing but unsigned ints for different bit sizes
  • isize/usize: int types with architecture dependent sizing (used for indexing collections)

Go

  • int, int8, int16, int32, int64: Signed ints for different bit sizes
  • unint...: same as before but unsigned

Both languages have fixed-size ints for better control over memory.

Floats/Complex

Rust

  • f32,f64: 32 and 64 bit floating point numbers

Go

  • float32, float64: the same as Rust
  • complex64, complex128 floats that can represent real and imaginary parts

Boolean

Rust

  • bool: true and false

Go

  • bool: true and false

Char/Rune

Rust

  • char: a single Unicode scalar value (special characters/emojis as well)

Go

  • rune: a single Unicode scalar value or a surrogate pair outside the Basic Mutilingual Plane

It is important to note that the char in Rust is guranteed to be a valid Unicode scalar point, but the rune in Go can be any Unicode code point. They are both 4 bytes. Go's rune is more flexible and can represnt a wider range of Unicode code points, but you have to check the validity of the Unicode scalar point values.

Rust Example:

let valid_char: char = 'A'; // Valid Unicode scalar value
let invalid_char: char = '\uD800'; // Invalid Unicode scalar value (lone surrogate)

// This will compile without errors:
println!("{}", valid_char);

// This will result in a compile-time error:
println!("{}", invalid_char);

Go Example:

var validRune rune = 'A' // Valid Unicode code point
var invalidRune rune = '\uD800' // Invalid Unicode scalar value (lone surrogate)

fmt.Println(validRune) // Output: A
fmt.Println(invalidRune) // Output: 55296 (the numerical value of the invalid code point)

Composite Types

Composite data types are data types that are constructed from combinations of primitive data types. They allow you to represent more complex structures and relationships between data elements.

Arrays

Arrays in Go and Rust both work the same: fixed-size collectinos of elements of the same type. Both support slicing, and passed by value (Rust has ownership rules). Rust arrays are immutable by default but can be made mutable using the mut keyword. Go arrays are mutable by default, and does not follow strict borrowing rules. It can be modified freely unless passed as a parameter then it is passed by value and results in a copy being made.

Rust Example: Mutable Array

let mut arr = [1, 2, 3];
arr[0] = 10;

Go Example:

arr := [3]int{1, 2, 3}
arr[0] = 10

Go has a garbage collector so we do not need to worry about deallocating and cleaning up ourselves, unlike Rust. That being said, Rust does use the stack as the default storage for arrays. Arrays in Go are value types, meaning when we pass it to a function it will be copied unless we use pass by reference.

Slices

Slices in Go are dynamic views of arrays and more commonly used. Go slices are more related to vectors as they can grow and shrink in size as needed. In Rust, slices are references to the data they point to and they do not own the data. Their size is dynamic and determined at runtime.

Go Slice Example:

arr := [3]int{1, 2, 3}
slice := arr[1:]

Rust Slice Example:

fn main() {
let mut arr = [1, 2, 3, 4, 5]; // Mutable array

let slice = &mut arr[1..4]; // Mutable slice
slice[0] = 10; // Modify the first element of the slice
println!("{:?}", arr); // Output: [1, 10, 3, 4, 5]
}

Struct

Both languages support structs or groups related data. They differ in features, memory management (surprise!), and flexibility.

Go Example

// Simple struct
type Person stuct {
Name string
Age int
}

// Instantiation and usage
person := Person{Name: "Alice", Age:30}
fmt.Println(person.Name)
person.Age = 31

// Methods on structs
func (p Person) Greet() string {
return "Hello, " + p.Name
}

// Pointer and Mutability
func (p *Person) HaveBirthday() {
p.Age += 1
}

In Go, structs are passed by value. If we want to modify the data in the struct, we must use a pointer.

Rust Example

// Struct definition
struct Person {
name: String,
age: u32,
}

//Instantiation and usage
let person = Person { name: Sting::from("Alice"), age: 30}; // ugly ass syntax
println!("{}", person.name);
let mut person = person;
person.age = 31;

// Methods on structs
impl Person {
fn greet(&self) -> String {
format!("Hello, {}", self.name)
}
}

//Ownership and borrowing Methods
impl Person {
fn have_birthday(&mut self) {
self.age += 1;
}
}
FeatureGo StructsRust Structs
SyntaxFields and types are defined like normal variablesFields are defined with type annotations
MutabilityFields are mutable by defaultFields are immutable by default; mut required for mutation
Memory SafetyRelies on garbage collectionNo garbage collection; uses ownership and borrowing for memory safety
Default ValuesFields have zero values when not initializedNo default values; all fields must be initialized
MethodsMethods defined with receivers (value or pointer)Methods defined inside impl blocks with self
Struct CompositionUses embedding for inheritance-like behaviorNo inheritance; composition via traits
Passing by Value/ReferenceStructs are passed by value unless a pointer is usedStructs are passed by value unless a reference (&) is used
Memory ManagementStructs are garbage collectedRust structs follow ownership and borrowing rules
Trait ImplementationsNo built-in trait systemSupports traits for behavior reuse across types

OOP

Both languages are not object oriented in the tradditional sense. Rust still adheres to the core pricniples of OOP through traits. Go is also object oriented-like through the use of interfaces.

Go OOP Example with Interfaces:

type Animal interface {
Speak()
}

type Dog struct {
Name string
}

func (d Dog) Speak() {
fmt.Println("Woof!")
}

type Cat struct {
Name string
}

func (c Cat) Speak() {
fmt.Println("Meow!")
}

func main() {
dog := Dog{Name: "Buddy"}
cat := Cat{Name: "Whiskers"}

var animal Animal

animal = dog
animal.Speak()

animal = cat
animal.Speak()
}

Rust Example with Traits:

trait Animal {
fn speak(&self);
}

struct Dog {
name: String,
}

impl Animal for Dog {
fn speak(&self) {
println!("Woof!");
}
}

struct Cat {
name: String,
}

impl Animal for Cat {
fn speak(&self) {
println!("Meow!");
}
}

fn main() {
let dog = Dog { name: "Buddy".to_string() };
let cat = Cat { name: "Whiskers".to_string() };

dog.speak();
cat.speak();
}

Bypassing MDMs on Arm Macs With RecoveryOS

· 3 min read

MacOS has a implemented a new security layer for their new M1 Macs. If you were to buy a Mac owned by a company that closed its doors and can not disable the MDM system, you would have some options on the Intel platform. Bootable USBs make it easy to reinstall MacOS, Windows, or Linux. With the new Macs, you would be left with a paper weight. This is not a perfect fix. After every major update, the MDM enrollment can reactivate. Rebooting can also lead to the same results and render the Mac useless.

Often times, companies sell Macs without removing the MDM or they go under and sell off their computers in bulk and forget about it.

Initial Boot into RecoveryOS

  1. Hold down the power on boot up and select RecoveryOS
  2. Launch the terminal

Disabling Platform Integrity Protection

PIP is a security mechanism that helps prevent unauthorized modifications to the system software and firmware. It's designed to protect the device from malicious software and unauthorized access. It really focuses on three things to ensure system integrity:

  1. Prevents Unauthorized Modifications: PIP prevents unauthorized changes to the system software and firmware, including the kernel, drivers, and system extensions. This helps protect the device from malware and other security threats.
  2. Enforces Code Signing: PIP requires all system software and firmware to be digitally signed by Apple or an authorized developer. This ensures that only trusted code can be executed on the device.
  3. Protects Boot Process: PIP protects the boot process from unauthorized modifications, preventing attackers from gaining control of the device before the operating system loads.

Here is some more info: https://support.apple.com/en-us/102149

In the terminal we can disable PIP:

csrutil disable

Edit Managed Client Property List

A property list is a file format used by Apple to store configuration data and settings for various applications and system components on MacOS.

They are used frequently in MacOS, we can see just how many starting at root here:

image

That's a lot of property lists. We can edit the property list that contains data about mobile device management.

sudo vi /System/Library/LaunchDaemons/com.apple.ManagedClient.enroll.plist

We need to change

<key>com.apple.ManagedClient.enroll</key>
<true/>

to be

<key>com.apple.ManagedClient.enroll</key>
<false/>

Blocking Additional Apple Servers

0.0.0.0 iprofiles.apple.com
0.0.0.0 mdmenrollment.apple.com
0.0.0.0 deviceenrollment.apple.com
0.0.0.0 gdmf.apple.com
0.0.0.0 acmdm.apple.com
0.0.0.0 albert.apple.com

Disable the Enrollment Service

sudo launchctl disable system/com.apple.ManagedClient.enroll

Additional Resets

# Resets the cloud configuration activation status.
sudo rm /var/db/ConfigurationProfiles/Settings/.cloudConfigHasActivationRecord

# Forces a re-search for cloud configurations.
sudo rm /var/db/ConfigurationProfiles/Settings/.cloudConfigRecordFound

# Indicates that a cloud configuration profile has been installed.
sudo touch /var/db/ConfigurationProfiles/Settings/.cloudConfigProfileInstalled

# Indicates that a cloud configuration record was not found.
sudo touch /var/db/ConfigurationProfiles/Settings/.cloudConfigRecordNotFound