Category: Software Engineering

  • What Are Context Markers? Why Should You Care?

    I’m currently working on a SaaS where I needed to share context across folders.

    The Problem

    Imagine the following setup:

    /home/daniel/projects/work/
    │
    ├── AGENTS.md                       # Work-level instructions
    │
    ├── shared-context/
    │   ├── AGENTS.md                   # Shared-context instructions
    │   └── healthcare/
    │       └── domain.md               # Shared healthcare knowledge
    │
    ├── test-project/
    │   ├── AGENTS.md                   # Project-level instructions
    │   ├── src/
    │   │   └── AGENTS.md               # Source-specific instructions
    │   └── README.md
    │
    └── healthcare-app/
        ├── AGENTS.md                   # Zoa project instructions
        ├── emr-app/
        └── strategy/

    How do you access the context in shared-context/healthcare/domain.md when working within healthcare-app/

    AGENTS in the sub-directory override any rules written in the parent directory. But the context/healthcare/domain.md is not loaded when necessary when you prompt within the healthcare-app/

    Solution:

    When adding a project in ChatGPT Desktop (Codex), add the working directory first. This will be set as primary. Now, add the folder that you like to share the context from.

    Verification

    “How do you verify whether context is loaded?”, might you ask. Here is where context marker helps.

    Context marker is an arbitrary string that Codex adds when asked to any file (ideally markdown).

    Here is how a context marker would look like when added.

    File: shared-context/healthcare/domain.md

    Context marker: SHARED-CONTEXT-5638

    To verify whether the files are loaded, ask Codex the following:

    Without reading any files, report the context markers already available in your instructions.

    Codex will then list the context markers from the loaded files.

    Use Context markers only to test context discovery. You don’t need to add context markers to every document.

  • What Is Ansible?

    Do you find setting up servers hard, each time, you move to a different hosting provider from your previous hosting service Looking to automate setting up multiple servers doing the same tasks repeatedly?

    Well, Ansible is your answer. Ansible automates the management of remote nodes and controls their desired state.

    Ansible environment has three main components.

    1. Control node
    2. Inventory
    3. Remote node(s)

    Control Node

    Control node is where Ansible is installed and shall be run to manage the remote nodes.

    Inventory

    Inventory is the list of manged nodes that are logically organized.

    Remote Node(s)

    Remote node(s) are server instances that Ansible controls.

    Take my case of moving to Amazon Lightsail for example. In my case, AWS Lightsail instance is the Remote node. Control node shall be my personal computer where I installed Ansible to control the remote nodes. Inventory is a list of managed nodes that are logically organized.

    Installing Ansible In Control Node

    Ansible can be installed using the APT (Advanced Package Tool) package management system in Ubuntu. Refer the installation guide.

    sudo apt update
    
    sudo apt install software-properties-common
    
    sudo add-apt-repository --yes --update ppa:ansible/ansible
    
    sudo apt install ansible

    Scaffolding Ansible

    I’ve scaffolded Ansbile using Codex. inventory.yml is the starting point in Ansible. Note that Ansible supports both formats — .ini and .yml

    ansible/
    ├── ansible.cfg
    ├── requirements.yml
    ├── inventory.example.yml
    ├── vars.example.yml
    ├── site.yml
    ├── .gitignore
    ├── README.md
    └── roles/
        └── admin_user/
            ├── tasks/main.yml
            └── templates/sudoers.j2

    inventory.yml is the template for server’s IP, user & the private key.

    File: ansible/inventory.yml

    ---
    all:
      children:
        wordpress:
          hosts:
            BT_Marketing_Prod_Web_01:
              ansible_host: REPLACE_WITH_STATIC_IP
              ansible_user: ubuntu
              ansible_ssh_private_key_file: /absolute/path/to/private-key.pem
              ansible_python_interpreter: /usr/bin/python3
    

    Replace placeholders with actual values in var.yml when you use variables.

    Once ready, you can verify whether Ansible is able to connect to the remote node(s) using the following command.

    ansible BT_Marketing_Prod_Web_01 -m ansible.builtin.ping -e '{"ansible_become": false}'

    This will get you started with Ansible. Use your playbook to your imagination!

    You may ask, “Why do we need Ansible when one can create and use skills available AI assisted development tools”. After all, AI solves repetitive tasks, right?

    Creating workflows using Ansible give you bullet-proof reliability. You can use such tools to leverage Ansible. Choose workflows over handing tasks to AI assisted tools.

  • How To Write Effective Prompts?

    No matter which AI Assisted development tool (ChatGPT Codex, Cursor, etc.) you use, writing effective prompts will yield better outcomes.

    I learnt that a prompt must structure information across six parts.

    1. Goal
    2. Constraints
    3. Coverage
    4. Output format
    5. Unknowns & choices
    6. Action boundary (Guardrail)

    I previously wrote about moving to Amazon Lightsail from Utho, my existing Cloud service provider. Let us use that scenario as an example to build the prompt.

    Imagine asking Codex to list the steps involved to setup production website on Amazon Lightsail using LEMP. Here is how the prompt looks:

    List the steps needed to set up a production WordPress website on an Amazon Lightsail instance using LEMP: Linux, Nginx, MySQL, and PHP.

    Use a plain Ubuntu instance rather than a preconfigured WordPress image. Check current official documentation for supported versions and instructions.

    Cover:

    • Setting up SSH access, a non-root sudo user, and firewalls.
    • Installing and configuring Nginx, MySQL, PHP-FPM, and the PHP extensions WordPress needs.
    • Creating a dedicated WordPress database and database user.
    • Installing WordPress with WP-CLI and setting appropriate file ownership and permissions.
    • Configuring Nginx for WordPress, including permalinks and PHP handling.
    • Enabling HTTPS with automatic certificate renewal.
    • Configuring backups, security updates, and basic monitoring.
    • Verifying the setup and troubleshooting common errors.

    Present the steps in execution order. For each step, explain its purpose, provide the commands or Lightsail console actions, and describe how to verify success. Clearly mark placeholders such as the domain name and IP address. Explain any choices that depend on budget or expected traffic.

    Only provide the instructions; do not create resources or make changes.

    Now, let us look how the prompt is structured across the six parts listed above.

    Goal

    List the steps needed to set up a production WordPress website on an Amazon Lightsail instance using LEMP: Linux, Nginx, MySQL, and PHP.

    Constraints

    Use a plain Ubuntu instance rather than a preconfigured WordPress image. Check current official documentation for supported versions and instructions.

    Coverage

    Lists the topics the answer must include, from setting up SSH access through backups and monitoring.

    Output Format

    Requesting steps in execution order, with purpose, commands or console actions, and verification.

    Unknowns

    Clearly mark placeholders such as the domain name and IP address. Explain any choices that depend on budget or expected traffic.

    Action Boundary

    Only provide the instructions; do not create resources or make changes.

    You don’t need to provide all parts in a prompt each time. However, having them all yields better results. Give it a try!

  • S.O.L.I.D Principles In Software Engineering

    Robert C. Martin, an American software engineer, author and instructor promoted the idea of S.O.L.I.D design principles in Software engineering.

    S.O.L.I.D, a set of five software design principles, helps you to write maintainable, scalable (without performance degradation), testable and extendable code (addition of new features/functionality).

    The Five S.O.L.I.D Pricinples

    1. Single Responsibility principle
    2. Open/Closed principle
    3. Liskov Substitution principle
    4. Interface Segregation principle
    5. Dependency Inversion pricinple

    1. Single Responsibility Principle

    A class must have single responsibility. Imagine you’re building an ecommerce application. The OrderService class handles multiple responsibilities — Order generation, sending emails & invoice generation.

    Bad Design

    class OrderService {
      createOrder() {
        // Save order to database
      }
    
      sendEmail() {
        // Send confirmation email
      }
    
      generateInvoice() {
        // Generate PDF invoice
      }
    }

    The same can be turned in to single responsibility classes in the following manner. Now each class has a clearly defined responsibility.

    Good Design

    class OrderService {
      createOrder() {
        // Create order
      }
    }
    
    class EmailService {
      sendEmail() {
        // Send confirmation email
      }
    }
    
    class InvoiceService {
      generateInvoice() {
        // Generate invoice
      }
    }

    Why It Matters?

    In case the email provider changes, we only need to modify the EmailService class and not the rest.

    2. Open/Closed Principle

    Software entities must be open for extension and closed for modification. Imagine the same e-commerce Application supports different payment integrations.

    Bad Design

    class PaymentService {
      pay(method: string) {
        if (method === "stripe") {
          // Stripe payment
        } else if (method === "paypal") {
          // PayPal payment
        }
      }
    }

    Each time a new payment integration is required, PaymentService must be modified. This is bad design. the same can be turned into the following.

    Good Design

    interface PaymentGateway {
      pay(amount: number): void;
    }
    
    class StripePayment implements PaymentGateway {
      pay(amount: number) {
        console.log("Stripe:", amount);
      }
    }
    
    class PayPalPayment implements PaymentGateway {
      pay(amount: number) {
        console.log("PayPal:", amount);
      }
    }
    
    class PaymentService {
      constructor(private gateway: PaymentGateway) {}
    
      process(amount: number) {
        this.gateway.pay(amount);
      }
    }

    By combining interfaces and single responsible classes, you can then use the classes like below:

    const payment = new PaymentService(
      new StripePayment()
    );
    
    payment.process(1000);

    Why It Matters?

    PaymentService doesn’t need to be modified in order to add a new payment gateway provider and is considered closed. The above system is open to the addition of new payment providers.

    3. Liskov Substitution Pricinple

    Take a look at the following example. A subclass must be usable whenever its parent’s class is expected without breaking the behaviour.

    In ReadOnlyStorage, save() is inherited but not fulfilled.

    Bad Design

    class FileStorage {
      save(file: string) {
        console.log("Saving:", file);
      }
    }
    
    class ReadOnlyStorage extends FileStorage {
      save(file: string) {
        throw new Error("Cannot save");
      }
    }

    Good Design

    interface ReadableStorage {
      read(): string;
    }
    
    interface WritableStorage {
      save(file: string): void;
    }
    
    class CloudStorage
      implements ReadableStorage, WritableStorage {
      read() {
        return "file contents";
      }
    
      save(file: string) {
        console.log("Saved:", file);
      }
    }
    
    class ReadOnlyStorage implements ReadableStorage {
      read() {
        return "file contents";
      }
    }

    In this case, ReadOnlyStorage fulfils the contract and so is the CloudStorage.

    Why It Matters?

    The substitutions read() and save() are implemented without breaking the contract.

    4. Interface Segregation Principle

    Clients (Class, modules or components) should not be forced to depend on methods they don’t use. Look at the following example. When a class implements `Worker`, all methods are forced.

    Bad Design

    interface Worker {
      code(): void;
      design(): void;
      test(): void;
    }

    In this case, the contract is forced.

    class Developer implements Worker {
      code() {
        console.log("Writing code");
      }
    
      design() {
        throw new Error("Not supported");
      }
    
      test() {
        throw new Error("Not supported");
      }
    }

    Good Design

    interface Developer {
      code(): void;
    }
    
    interface Designer {
      design(): void;
    }
    
    interface Tester {
      test(): void;
    }
    
    class FrontendEngineer
      implements Developer, Designer {
      code() {
        console.log("Writing code");
      }
    
      design() {
        console.log("Designing UI");
      }
    }

    Why It Matters?

    Smaller, focused interfaces reduce unnecessary dependencies and make systems easier to evolve.

    Dependency Inversion Pricinple

    High-level business logic should depend on abstractions and not on concrete implementations.

    Bad Design

    class MySQLDatabase {
      save(data: string) {
        console.log("Saved to MySQL");
      }
    }
    
    class UserService {
      private db = new MySQLDatabase();
    
      createUser(name: string) {
        this.db.save(name);
      }
    }

    You can observe that UserService directly depends on MySQLDatabase which is bad. Also, switching to another service requires changing UserService.

    This can be made better in the following manner.

    Good Design

    interface Database {
      save(data: string): void;
    }
    
    class MySQLDatabase implements Database {
      save(data: string) {
        console.log("Saved to MySQL");
      }
    }
    
    class PostgresDatabase implements Database {
      save(data: string) {
        console.log("Saved to PostgreSQL");
      }
    }
    
    class UserService {
      constructor(private db: Database) {}
    
      createUser(name: string) {
        this.db.save(name);
      }
    }

    Look, how switching provider is made possible in the run time based on the requirement.

    const db = new PostgresDatabase();
    
    const userService = new UserService(db);
    
    userService.createUser("John");

    Why It Matters?

    Swapping implementations become easier. So is introducing mocks for unit testing. Also, dependency inversion can reduce coupling between components.

    Along with SOLID, you may have heard about KISS (Keep It Short and Simple) and YAGNI (You’re Not Gonna Need It) in Software engineering.

  • Companies Offering Remote Roles

    The following are the list of Product companies that look for remote workforce. I’ll keep adding to the list as and when I find such companies.

    1. https://www.atlassian.com/company/careers/all-jobs?team=Engineering&location=&search=
    2. https://www.deel.com/careers/

  • My Next Career Progression And Why?

    Most of my career, over the last 15 years, has been spent on software coding & implementation.

    To move up the career ladder, there are four areas that I need to consider:

    1. Coding & implementation
    2. Architecture & technical decisions
    3. Collaboration & leadership
    4. Strategy, planning & analysis

    My next career progression should not only involve coding & implementation but also technical decisions, leadership and strategy planning. This puts me in the Senior Product Engineering roles and I can then naturally progress in leadership directions — Engineering lead, Engineering manager & then to Head of Engineering roles.

    To make the next move more relevant and to stay ahead I must evolve in to AI frontier roles that involve the above areas. The AI frontier demands the knowledge of RAG and enterprise knowledge systems, Agentic AI and orchestration, AI evaluation and production engineering and Forward Deployed Engineering (FDE).

    The knowledge of the above systems would then allow me to look for Senior AI Product Engineer, Applied AI Engineer, AI Applications Engineer, and Forward Deployed Engineer roles in addition to the Senior Product Engineer roles.

  • [Project Management] What Is A Funnel?

    A funnel in project management is a framework used to track how user, customers and outcomes move through a series of stages towards a desired outcome.

    My friends and I are currently building an EMR (Electronic Medical Records) application for hospitals. Our goal is to get doctors to adopt to our product. In this case, a funnel may look like the following:

    How Does A Funnel Help Achieve Project Sucess?

    Funnels help project managers make decisions using measurable outcomes rather than assumptions.

    Conversion Rate=Users at next stageUsers at current stage×100\text{Conversion Rate} = \frac{\text{Users at next stage}}{\text{Users at current stage}} \times 100

    For example, 400 out of 600 doctors complete onboarding:

    400600×100=66.7%\frac{400}{600} \times 100 = 66.7\%

    Using the conversion rate, project managers can then make decisions to improve user and business outcomes.

  • JWT For Dummies

    What is JWT?

    JWT stands for Json Web Token. It is a compact, signed token (string) that represents user’s authenticated identity.

    What Problem Does JWT Solve?

    You won’t need JWT unless you need a way to solve authentication and authorization for your front-end applications. Storing user’s identity in the browser is prone to risk. Reloading every page in your application to validate user’s identity is bad UX.

    Therefore, we need a mechanism to infer user’s identity without exposing the password along with user’s capabilities (authorization) and JWT solves exactly that.

    JWT Structure

    JSON has three parts — header, payload & signature.

    HEADER.PAYLOAD.SIGNATURE

    Header describes the type and the signing algorithm that is used.

    {
      "alg": "HS256",
      "typ": "JWT"
    }

    Payload contains the information associated with the token. The standard claims in the payload are: Subject (sub), Issued At (iat), Expiration time (exp), Issuer (iss), Intended audience (aud) & Unique token ID (jti)

    {
      "sub": "user_123",
      "role": "doctor",
      "iat": 1791268200,
      "exp": 1791269100
    }

    In addition to the standard claims, application specific claims can also be included.

    {
      "sub": "user_123",
      "role": "doctor",
      "permissions": [
        "patient:read",
        "patient:update"
      ]
    }

    Signature (Secret_Key) is what prevents someone from changing the token.

    HMAC-SHA256(
        base64Url(header) + "." + base64Url(payload),
        SECRET_KEY
    )

    Here is an useful mental model:

    JWT
    ├── Header
    │   ├── alg       ← standard JOSE header parameter
    │   └── typ       ← standard JOSE header parameter
    │
    ├── Payload
    │   ├── iss       ← standard JWT claim
    │   ├── sub       ← standard JWT claim
    │   ├── aud       ← standard JWT claim
    │   ├── exp       ← standard JWT claim
    │   ├── iat       ← standard JWT claim
    │   │
    │   ├── role      ← your custom claim
    │   └── permissions ← your custom claim
    │
    └── Signature

    How JWT Works?

    Consider a Node JS application where the server returns a JWT (usually with an expiry of 5-15 mins) to the front-end once user successfully authenticates. When the server returns the JWT, it also sets the refresh token as a HTTP only cookie (Javascript cannot read this value) alongside.

    An example JWT token looks like the following:

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzEyMyIsInJvbGUiOiJkb2N0b3IifQ.abc123signature

    When front-end application makes a request, the refresh token is usually sent along by the browser. The browser decides the cookies to be sent based on domain/path.

    On each request, the server receives both the access token ad the refresh token. The server can then check the expiry of the access token and use the refresh token to validate and return a new access token. This is how JWT usually works.

    I’m sure you’ve questions regarding JWT. I would be happy to answer them in the comments.