<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Endowus Build Notes</title>
    <subtitle>Product and Engineering at Endowus</subtitle>
    <link rel="self" type="application/atom+xml" href="/atom.xml"/>
    <link rel="alternate" type="text/html" href="/"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-06-30T00:00:00+00:00</updated>
    <id>/atom.xml</id>
    
        
    <entry xml:lang="en">
        <title>Introducing EndowusAI: Problems Before Predictions</title>
        <published>2026-06-30T00:00:00+00:00</published>
        <updated>2026-06-30T00:00:00+00:00</updated>
        <author>
          <name>
            
              Endowus
            
          </name>
        </author>
        <link rel="alternate" type="text/html" href="/endowusai-launch/"/>
        <id>/endowusai-launch/</id>
        
        <summary type="html"><h2 id="why-good-ai-guidance-in-wealth-management-starts-with-the-problem-not-the-model">Why good AI guidance in wealth management starts with the problem, not the model</h2>
<p>A year ago, the fastest way to look innovative in wealth management was to bolt a chatbot about market commentary onto your product and call it AI. The market obliged. Tools were built, interest was assumed, and a strange focus formed around using AI to predict the market, reading sentiment and signals to drive transactions. The “AI model” was the product, and whoever had the most sophisticated one would win.</p>
<p>We don’t believe that.</p>
<p>And the reality only underscored the point. When capability is turned into a commodity and while the industry is still working out what makes commercial sense, the thing that sets you apart and stays consistent is the philosophy you encode into it.</p>
<p>The specific area we chose to focus on is helping a real person define a goal they can realistically fund, and understand more intuitively how to best use our platform, not producing more data for someone to process. As Morgan Housel puts it, <a rel="noopener external" target="_blank" href="https://harriman-house.com/authors/morgan-housel/the-psychology-of-money/9780857197689">the soft skills of money matter more than the technical side</a>. A model that answers market questions well but doesn’t encourage good behaviour is more informational bloat in a domain that demands a fiduciary.</p></summary>
        
    </entry>
    
        
    <entry xml:lang="en">
        <title>How We Built an AI-Assisted Sentry Digest to Reduce Alert Fatigue</title>
        <published>2026-03-19T00:00:00+00:00</published>
        <updated>2026-03-19T00:00:00+00:00</updated>
        <author>
          <name>
            
              Endowus
            
          </name>
        </author>
        <link rel="alternate" type="text/html" href="/sentry-slack-digests/"/>
        <id>/sentry-slack-digests/</id>
        
        <summary type="html"><p>At Endowus, hundreds of Sentry notifications flow daily into dedicated Slack alert channels. These alerts are essential for reliability and incident response, but keeping up with the volume is a different challenge altogether. We’ve written about our Sentry &amp; Slack alerting setup in a previous post: <a href="/sentry-bff-alerts/">Solving the Shared Ownership Alerting Challenge</a>.</p>
<p>Our initial setup worked well, but as alert volumes grew, the strain started to show. During a particularly busy period of releases and activity, working closely with our engineering teams as a Tech Program Manager, I could see how much time and attention was being diverted from product development into manually reviewing alerts. Useful signals were present — recurring errors, gradual spikes, subtle correlations — but extracting them required sustained focus across multiple Slack channels and time windows.</p>
<p>Although I’m not a developer, I wanted to help the team solve this. Using AI-assisted development tools, I built an initial working prototype myself, iterating on data extraction, formatting and analysis logic. That first version made the concept tangible: structured interpretation could reduce review effort without disrupting existing monitoring workflows.</p>
<p>Building the prototype also sharpened the problem definition. The alerts themselves were not broken: they were working exactly as intended. What was missing was a structured way to interpret them at scale.</p></summary>
        
    </entry>
    
        
    <entry xml:lang="en">
        <title>The Endowus Engineering Career Ladder</title>
        <published>2026-01-10T00:00:00+00:00</published>
        <updated>2026-01-10T00:00:00+00:00</updated>
        <author>
          <name>
            
              Endowus
            
          </name>
        </author>
        <link rel="alternate" type="text/html" href="/ic-career-path/"/>
        <id>/ic-career-path/</id>
        
        <summary type="html"><p>At Endowus, we believe that “growth” is multidimensional.</p>
<p><strong>At its core, growth is defined by increased levels of ownership and impact.</strong></p>
<p>It isn’t just about changing your job title. Real growth happens when you take on end-to-end ownership of a product feature, when you master a new functional domain (like pension schemes or private markets), or when you learn to architect systems that can handle millions of dollars in transactions without flinching. It happens through mentoring peers and collaborating across teams.</p>
<p>Of course, clear career progression is a vital part of that picture. We want every engineer to know where they stand and what the next step looks like.</p>
<p>One of the most common questions we get is: <em>“Do I have to become a manager to grow?”</em></p>
<p>The answer is <strong>No</strong>.</p></summary>
        
    </entry>
    
        
            
        
    <entry xml:lang="en">
        <title>From Thousands of Files to Just Seven: A Data Lakehouse Optimization Story</title>
        <published>2025-11-26T00:00:00+00:00</published>
        <updated>2025-11-26T00:00:00+00:00</updated>
        <author>
          <name>
            
              Endowus
            
          </name>
        </author>
        <link rel="alternate" type="text/html" href="/data-lakehouse-optimization/"/>
        <id>/data-lakehouse-optimization/</id>
        
        <summary type="html"><p>At Endowus, data is at the heart of everything we do, from personalizing client experiences to powering our investment insights. As our platform grows, so does our data lakehouse, and with that growth comes the inevitable engineering challenges of managing scale, cost, and performance. Recently, our Data Platform team embarked on a mission to tackle two critical issues that were impacting our data pipelines: the notorious “small file problem” and the crucial challenge of data freshness.</p>
<p>This post shares our journey of diagnosing these challenges, implementing a multi-faceted solution using Delta Lake, and the dramatic improvements we achieved in performance, cost, and reliability. It’s a story about how rethinking the physical layout of our data unlocked efficiencies across the entire platform.</p></summary>
        
    </entry>
    
        
    <entry xml:lang="en">
        <title>A Summer at Endowus: My Journey as a Software Engineer Intern</title>
        <published>2025-09-25T00:00:00+00:00</published>
        <updated>2025-09-25T00:00:00+00:00</updated>
        <author>
          <name>
            
              Endowus
            
          </name>
        </author>
        <link rel="alternate" type="text/html" href="/summer-intern-experience/"/>
        <id>/summer-intern-experience/</id>
        
        <summary type="html"><p><em><a rel="noopener external" target="_blank" href="https://www.linkedin.com/in/ying-xuan-shernice-sng/">Shernice Sng</a> is a Year 3 Computer Science undergraduate at the National University of Singapore who recently completed a summer internship with the Endowus engineering team.</em></p>
<p>Over the past summer, I had the privilege of joining Endowus as a Software Engineer Intern. For three months, I contributed to meaningful projects, learned from talented colleagues, and grew both professionally and personally.</p></summary>
        
    </entry>
    
        
    <entry xml:lang="en">
        <title>Implementing Distributed Tracing in a Polyglot Microservices Environment</title>
        <published>2025-04-14T00:00:00+00:00</published>
        <updated>2025-04-14T00:00:00+00:00</updated>
        <author>
          <name>
            
              Endowus
            
          </name>
        </author>
        <link rel="alternate" type="text/html" href="/distributed-tracing-log-correlation/"/>
        <id>/distributed-tracing-log-correlation/</id>
        
        <summary type="html"><p>Distributed Tracing is an extremely useful tool to improve observability of distributed systems. By visualizing the entire journey of a request—from the moment a customer clicks a button on a Next.js front end or Flutter mobile app, through the Nest.js BFF and Scala backend services using Akka and Kafka—distributed tracing empowers developers to quickly diagnose performance issues, identify bottlenecks, and reduce mean time to resolution (MTTR). This article details our technical journey, the challenges, explains the underlying concepts, and offers an endowus point of view of us implementing it.</p></summary>
        
    </entry>
    
        
    <entry xml:lang="en">
        <title>Solving the Shared Ownership Alerting Challenge</title>
        <published>2025-01-19T00:00:00+00:00</published>
        <updated>2025-01-19T00:00:00+00:00</updated>
        <author>
          <name>
            
              Endowus
            
          </name>
        </author>
        <link rel="alternate" type="text/html" href="/sentry-bff-alerts/"/>
        <id>/sentry-bff-alerts/</id>
        
        <content type="html" xml:base="/sentry-bff-alerts/"><p>At Endowus, our tech ecosystem is built on a microservices architecture, with multiple stream-aligned teams owning subsets of the backend services. These teams are responsible for the features and domain capabilities that power our frontend web and mobile applications.</p>
<p>To connect these applications with our microservices, we rely on a Backend for Frontend (BFF) platform. Built with the <a rel="noopener external" target="_blank" href="https://docs.nestjs.com/">NestJS</a> framework, the BFF acts as a gateway, exposing modularized endpoints that aggregate and simplify interactions between frontend and backend layers.</p>
<p>This platform, while central to our architecture, is inherently a shared component. Each endpoint is tied to specific backend services owned by different teams. This shared ownership introduces complexity when managing operational concerns, such as error monitoring and alerting. How do we ensure that when something goes wrong, the right team is notified and can take action promptly?</p>
<h2 id="the-challenge-clear-ownership-in-a-shared-platform">The Challenge: Clear Ownership in a Shared Platform</h2>
<p>With multiple teams relying on the BFF platform, accountability for production issues becomes a challenge. Consider the typical lifecycle of a production incident:</p>
<ol>
<li>A user encounters an issue that generates an error.</li>
<li>The error is logged and sent as a <a rel="noopener external" target="_blank" href="https://sentry.io/">Sentry</a> alert.</li>
<li>Teams need to triage the alert and resolve the issue quickly.</li>
</ol>
<p>Without clear routing of alerts, issues like the following can arise:</p>
<ul>
<li><strong>Missed Alerts:</strong> Alerts may not reach the correct team if ownership isn’t explicitly defined, delaying resolution.</li>
<li><strong>Alert Fatigue:</strong> Teams receiving irrelevant alerts can become desensitized, leading to genuine issues being overlooked.</li>
<li><strong>Inconsistent Processes:</strong> With no standard mechanism, different teams might implement custom solutions, increasing maintenance overhead and reducing cohesion.</li>
</ul>
<p>What’s needed is a system that enforces ownership and guarantees that every alert is automatically routed to the appropriate team without manual intervention.</p>
<h2 id="our-approach-building-a-team-based-alerting-mechanism">Our Approach: Building a Team-Based Alerting Mechanism</h2>
<p>To address these challenges, we designed and implemented a robust alert routing mechanism within the BFF platform.</p>
<p>These were our design considerations:</p>
<ol>
<li><strong>Enforcing Ownership:</strong><br />
Each endpoint in the BFF must be explicitly associated with a team. This metadata must be mandatory, ensuring that no endpoint is left unowned.</li>
<li><strong>Tagging Alerts with Metadata:</strong><br />
We leverage Sentry’s tagging functionality to include team metadata in all alerts. This makes it possible to programmatically route alerts based on ownership and filter them in Sentry’s dashboard.</li>
<li><strong>Seamless Developer Experience:</strong><br />
Adding team metadata to endpoints must be straightforward for developers. We integrated this requirement into our NestJS modules in a way that aligns with existing workflows, reducing cognitive load and increasing adoption.</li>
<li><strong>Pre-Runtime Validation:</strong><br />
To prevent misconfigurations, we implemented a validation step that ensures all endpoints are tagged with team metadata before deployment. This guarantees no alert falls through the cracks.</li>
</ol>
<p>This approach not only resolves the immediate alerting problem but also lays the foundation for additional metadata-driven validations, such as enforcing domain boundaries and public endpoint checks.</p>
<h2 id="alerting-mechanism-overview">Alerting Mechanism Overview</h2>
<p>The flow begins when a request is received by the controller.</p>
<ol>
<li>The controller processes and tags the request with team-specific metadata.</li>
<li>If an error occurs during the request’s lifecycle, an exception event is generated and captured by the Sentry App.</li>
<li>The Sentry App is configured to automatically trigger and route a corresponding alert to the relevant team’s Slack channel.</li>
</ol>
<figure>
  <img src="image-request-flow-diagram.png" alt="Request flow diagram">
  <figcaption style="text-align: center">Request flow diagram</figcaption>
</figure>
<h2 id="implementation-details">Implementation Details</h2>
<p>Let’s dive into the practical implementation of this functionality within our NestJS BFF. We’ll walk through each step, highlighting key NestJS concepts along the way. For clarity, we’ll illustrate with an example involving three teams: Team A, Team B, and Team C..</p>
<h3 id="1-creating-the-team-decorator">1. Creating the Team Decorator</h3>
<p>First, we create a custom NestJS decorator that allows developers to bind team metadata to a method.</p>
<pre class="giallo" style="color: #D3C6AA; background-color: #2D353B;"><code data-lang="javascript"><span class="giallo-l"><span style="color: #859289;font-style: italic;">// team.decorator.ts</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: #83C092;">export</span><span style="color: #E67E80;"> enum</span><span style="color: #7FBBB3;"> TeamTag</span><span> {</span></span>
<span class="giallo-l"><span>   TeamA</span><span style="color: #E69875;"> =</span><span style="color: #DBBC7F;"> &#39;team-a&#39;</span><span>,</span></span>
<span class="giallo-l"><span>   TeamB</span><span style="color: #E69875;"> =</span><span style="color: #DBBC7F;"> &#39;team-b&#39;</span><span>,</span></span>
<span class="giallo-l"><span>   TeamC</span><span style="color: #E69875;"> =</span><span style="color: #DBBC7F;"> &#39;team-c&#39;</span><span>,</span></span>
<span class="giallo-l"><span> }</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: #83C092;"> export</span><span style="color: #E69875;"> const</span><span style="color: #A7C080;"> Team</span><span style="color: #E69875;"> =</span><span> (tag</span><span style="color: #E69875;">:</span><span style="color: #7FBBB3;"> TeamTag</span><span>)</span><span style="color: #E69875;">:</span><span style="color: #7FBBB3;"> CustomDecorator</span><span>&lt;</span><span style="color: #7FBBB3;">string</span><span>&gt;</span><span style="color: #E69875;"> =&gt;</span><span> {</span></span>
<span class="giallo-l"><span style="color: #E67E80;">   return</span><span style="color: #A7C080;"> SetMetadata</span><span>(</span><span style="color: #DBBC7F;">&#39;TEAM_TAG_METADATA_KEY&#39;</span><span>, tag);</span></span>
<span class="giallo-l"><span> };</span></span></code></pre><h3 id="2-developer-usage-decorating-the-controllers-with-team-metadata">2. Developer Usage: Decorating the Controllers with Team Metadata</h3>
<p>In NestJS, controllers are components in which endpoints are defined. Developers can attach team metadata to these controllers by using the <code>Team</code> Decorator. For instance, a developer from Team A would apply the decorator as follows:</p>
<pre class="giallo" style="color: #D3C6AA; background-color: #2D353B;"><code data-lang="javascript"><span class="giallo-l"><span style="color: #83C092;">@</span><span style="color: #A7C080;">Team</span><span>(TeamTag</span><span style="color: #859289;">.</span><span>TeamA)</span></span>
<span class="giallo-l"><span style="color: #83C092;">@</span><span style="color: #A7C080;">Controller</span><span>({ path</span><span style="color: #859289;">:</span><span style="color: #DBBC7F;">&#39;team-a-service&#39;</span><span>, version</span><span style="color: #859289;">:</span><span style="color: #DBBC7F;"> &#39;1&#39;</span><span> })</span></span>
<span class="giallo-l"><span style="color: #83C092;">export</span><span style="color: #E67E80;"> class</span><span style="color: #7FBBB3;"> TeamAController</span><span> {</span></span>
<span class="giallo-l"><span style="color: #859289;font-style: italic;">  // ...controller endpoint methods</span></span>
<span class="giallo-l"><span>}</span></span></code></pre>
<p>Note: For greater flexibility, we can also override the controller level metadata by applying the Team Decorator to individual methods.</p>
<pre class="giallo" style="color: #D3C6AA; background-color: #2D353B;"><code data-lang="javascript"><span class="giallo-l"><span style="color: #83C092;">@</span><span style="color: #A7C080;">Team</span><span>(TeamTag</span><span style="color: #859289;">.</span><span>TeamB)</span></span>
<span class="giallo-l"><span style="color: #83C092;">@</span><span style="color: #A7C080;">Get</span><span>(</span><span style="color: #DBBC7F;">&#39;data-endpoint&#39;</span><span>)</span></span>
<span class="giallo-l"><span>async</span><span style="color: #A7C080;"> getData</span><span>(@</span><span style="color: #A7C080;">Query</span><span>() queryParams: QueryParams): </span><span style="color: #7FBBB3;">Promise</span><span style="color: #E69875;">&lt;</span><span>ResponseData</span><span style="color: #E69875;">&gt;</span><span> {</span></span>
<span class="giallo-l"><span>  return this.teamBService.getData(queryParams);</span></span>
<span class="giallo-l"><span>}</span></span></code></pre><h3 id="3-adding-team-metadata-to-the-request-payload">3. Adding Team Metadata to the Request Payload</h3>
<p>Bundling the metadata with the request payload will allow us to cohesively reference this data for our alert routing scenario.</p>
<p>To accomplish this, we’ll use a NestJS guard that intercepts the request pre-controller and adds the team metadata to the request payload for use later.</p>
<pre class="giallo" style="color: #D3C6AA; background-color: #2D353B;"><code data-lang="javascript"><span class="giallo-l"><span style="color: #859289;font-style: italic;">// add-team-tag.guard.ts</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: #83C092;">@</span><span style="color: #A7C080;">Injectable</span><span>()</span></span>
<span class="giallo-l"><span style="color: #83C092;">export</span><span style="color: #E67E80;"> class</span><span style="color: #7FBBB3;"> AddTeamTagGuard</span><span style="color: #E69875;"> implements</span><span style="color: #7FBBB3;"> CanActivate</span><span> {</span></span>
<span class="giallo-l"><span style="color: #E69875;">  constructor</span><span>(</span><span style="color: #E69875;">private readonly</span><span> reflector</span><span style="color: #E69875;">:</span><span style="color: #7FBBB3;"> Reflector</span><span>) {}</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: #A7C080;">  canActivate</span><span>(context</span><span style="color: #E69875;">:</span><span style="color: #7FBBB3;"> ExecutionContext</span><span>)</span><span style="color: #E69875;">:</span><span style="color: #7FBBB3;"> boolean</span><span> {</span></span>
<span class="giallo-l"><span style="color: #E69875;">    const</span><span> req</span><span style="color: #E69875;"> =</span><span> context</span><span style="color: #859289;">.</span><span style="color: #A7C080;">switchToHttp</span><span>()</span><span style="color: #859289;">.</span><span style="color: #A7C080;">getRequest</span><span>();</span></span>
<span class="giallo-l"><span style="color: #E69875;">    const</span><span> handler</span><span style="color: #E69875;"> =</span><span> context</span><span style="color: #859289;">.</span><span style="color: #A7C080;">getHandler</span><span>();</span></span>
<span class="giallo-l"><span style="color: #E69875;">    const</span><span> endpointClass</span><span style="color: #E69875;"> =</span><span> context</span><span style="color: #859289;">.</span><span style="color: #A7C080;">getClass</span><span>();</span></span>
<span class="giallo-l"><span style="color: #E69875;">    const</span><span> team</span><span style="color: #E69875;"> =</span><span style="color: #D699B6;"> this</span><span style="color: #859289;">.</span><span>reflector</span><span style="color: #859289;">.</span><span style="color: #A7C080;">getAllAndOverride</span><span>(TEAM_TAG_METADATA_KEY, [handler, endpointClass]);</span></span>
<span class="giallo-l"><span>    req</span><span style="color: #859289;">.</span><span>team</span><span style="color: #E69875;"> =</span><span> team;</span></span>
<span class="giallo-l"><span style="color: #E67E80;">    return</span><span style="color: #D699B6;"> true</span><span>;</span></span>
<span class="giallo-l"><span>  }</span></span>
<span class="giallo-l"><span>}</span></span></code></pre>
<p>To enable the guard for all controllers, we add it as a global guard in the main module file (<code>app.module.ts</code>). Global guards will apply to all controllers within the main module.</p>
<pre class="giallo" style="color: #D3C6AA; background-color: #2D353B;"><code data-lang="javascript"><span class="giallo-l"><span style="color: #859289;font-style: italic;">// app.module</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: #E69875;">...</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: #83C092;">providers</span><span style="color: #859289;">:</span><span> [</span></span>
<span class="giallo-l"><span>  { provide</span><span style="color: #859289;">:</span><span> APP_GUARD, useClass</span><span style="color: #859289;">:</span><span> AddTeamTagGuard },</span></span>
<span class="giallo-l"><span>]</span></span></code></pre><h3 id="4-implementing-exception-handling-alert-logic">4. Implementing Exception Handling Alert Logic</h3>
<p>We implement a NestJS exception filter which will handle errors by sending an exception event to the Sentry App.</p>
<p>Using the official package <code>@sentry/node</code>, we can easily generate and capture Sentry events. Relevant fields, including the team metadata, are added to the event scope. This is essential for Slack Channel routing done by the Sentry Application in later steps.</p>
<pre class="giallo" style="color: #D3C6AA; background-color: #2D353B;"><code data-lang="javascript"><span class="giallo-l"><span style="color: #859289;font-style: italic;">// exceptions.filter.ts</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: #83C092;">import</span><span> { ArgumentsHost, Catch, ExceptionFilter }</span><span style="color: #E67E80;"> from</span><span style="color: #DBBC7F;"> &#39;@nestjs/common&#39;</span><span>;</span></span>
<span class="giallo-l"><span style="color: #83C092;">import</span><span style="color: #D699B6;"> *</span><span style="color: #E67E80;"> as</span><span> Sentry</span><span style="color: #E67E80;"> from</span><span style="color: #DBBC7F;"> &#39;@sentry/node&#39;</span><span>;</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: #83C092;">@</span><span style="color: #A7C080;">Catch</span><span>()</span></span>
<span class="giallo-l"><span style="color: #83C092;">export</span><span style="color: #E67E80;"> class</span><span style="color: #7FBBB3;"> AllExceptionFilter</span><span style="color: #E69875;"> implements</span><span style="color: #7FBBB3;"> ExceptionFilter</span><span> {</span></span>
<span class="giallo-l"><span style="color: #A7C080;">  catch</span><span>(exception</span><span style="color: #E69875;">:</span><span style="color: #7FBBB3;"> any</span><span>, host</span><span style="color: #E69875;">:</span><span style="color: #7FBBB3;"> ArgumentsHost</span><span>)</span><span style="color: #E69875;">:</span><span style="color: #7FBBB3;"> any</span><span> {</span></span>
<span class="giallo-l"><span style="color: #E69875;">    const</span><span> ctx</span><span style="color: #E69875;"> =</span><span> host</span><span style="color: #859289;">.</span><span style="color: #A7C080;">switchToHttp</span><span>(); </span></span>
<span class="giallo-l"><span>    Sentry</span><span style="color: #859289;">.</span><span style="color: #A7C080;">withScope</span><span>((scope)</span><span style="color: #E69875;"> =&gt;</span><span> {</span></span>
<span class="giallo-l"><span style="color: #E69875;">      const</span><span> request</span><span style="color: #E69875;"> =</span><span> ctx</span><span style="color: #859289;">.</span><span style="color: #A7C080;">getRequest</span><span>();</span></span>
<span class="giallo-l"><span>      scope</span><span style="color: #859289;">.</span><span style="color: #A7C080;">setTag</span><span>(</span><span style="color: #DBBC7F;">&#39;team&#39;</span><span>, request</span><span style="color: #859289;">.</span><span>team);</span></span>
<span class="giallo-l"><span>      Sentry</span><span style="color: #859289;">.</span><span style="color: #A7C080;">captureException</span><span>(exception);</span></span>
<span class="giallo-l"><span>    }); </span></span>
<span class="giallo-l"><span style="color: #E69875;">    const</span><span> response</span><span style="color: #E69875;"> =</span><span> ctx</span><span style="color: #859289;">.</span><span style="color: #A7C080;">getResponse</span><span>(); </span></span>
<span class="giallo-l"><span style="color: #E67E80;">    return</span><span> response</span><span style="color: #859289;">.</span><span style="color: #A7C080;">status</span><span>(exception</span><span style="color: #859289;">.</span><span>statusCode)</span><span style="color: #859289;">.</span><span style="color: #A7C080;">json</span><span>(exception);</span></span>
<span class="giallo-l"><span>  }</span></span>
<span class="giallo-l"><span>}</span></span></code></pre>
<p>As with the global guard previously, we enable the filter as a global filter for all modules.</p>
<pre class="giallo" style="color: #D3C6AA; background-color: #2D353B;"><code data-lang="javascript"><span class="giallo-l"><span style="color: #859289;font-style: italic;">// main.ts</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: #83C092;">import</span><span> { AppModule }</span><span style="color: #E67E80;"> from</span><span style="color: #DBBC7F;"> &#39;./app.module&#39;</span><span>;</span></span>
<span class="giallo-l"><span style="color: #E69875;">const</span><span> app</span><span style="color: #E69875;"> =</span><span style="color: #E67E80;"> await</span><span> NestFactory</span><span style="color: #859289;">.</span><span style="color: #A7C080;">create</span><span>(AppModule);</span></span>
<span class="giallo-l"><span>app</span><span style="color: #859289;">.</span><span style="color: #A7C080;">useGlobalFilters</span><span>(</span><span style="color: #E67E80;">new</span><span style="color: #A7C080;"> AllExceptionFilter</span><span>());</span></span></code></pre><h3 id="5-enforcing-ownership-through-validation">5. Enforcing Ownership through Validation</h3>
<p>While decorators offer a convenient way to specify team ownership, it’s easy for developers to overlook them. To ensure no endpoint is left untagged, we implement a validation module that runs during application startup.</p>
<p>The validation logic utilizes internal NestJS modules to iterate through each method/route:</p>
<ul>
<li><strong><code>DiscoveryService</code></strong>: Traverses the NestJS app module graph and retrieves submodule components of the application</li>
<li><strong><code>MetadataScanner</code></strong>: Scans and retrieves metadata from a submodule component</li>
</ul>
<p>During application startup, <code>ModuleValidationService.validate</code> is invoked. <code>DiscoveryService</code> and <code>MetadataScanner</code> iterate through the controller routes and retrieve team metadata.  If any endpoint is missing the required metadata, the application will gracefully terminate, preventing deployment of a misconfigured BFF.</p>
<pre class="giallo" style="color: #D3C6AA; background-color: #2D353B;"><code data-lang="javascript"><span class="giallo-l"><span style="color: #859289;font-style: italic;">// module-validation.service.ts</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: #83C092;">import</span><span> { Injectable }</span><span style="color: #E67E80;"> from</span><span style="color: #DBBC7F;"> &#39;@nestjs/common&#39;</span><span>;</span></span>
<span class="giallo-l"><span style="color: #83C092;">import</span><span> { Controller, Type }</span><span style="color: #E67E80;"> from</span><span style="color: #DBBC7F;"> &#39;@nestjs/common/interfaces&#39;</span><span>;</span></span>
<span class="giallo-l"><span style="color: #83C092;">import</span><span> { DiscoveryService, MetadataScanner, Reflector }</span><span style="color: #E67E80;"> from</span><span style="color: #DBBC7F;"> &#39;@nestjs/core&#39;</span><span>;</span></span>
<span class="giallo-l"><span style="color: #83C092;">import</span><span> { InstanceWrapper }</span><span style="color: #E67E80;"> from</span><span style="color: #DBBC7F;"> &#39;@nestjs/core/injector/instance-wrapper&#39;</span><span>;</span></span>
<span class="giallo-l"><span style="color: #83C092;">import</span><span> { Subject }</span><span style="color: #E67E80;"> from</span><span style="color: #DBBC7F;"> &#39;rxjs&#39;</span><span>;</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: #83C092;">@</span><span style="color: #A7C080;">Injectable</span><span>()</span></span>
<span class="giallo-l"><span style="color: #83C092;">export</span><span style="color: #E67E80;"> class</span><span style="color: #7FBBB3;"> ModuleValidationService</span><span> {</span></span>
<span class="giallo-l"><span style="color: #E69875;">  constructor</span><span>(</span></span>
<span class="giallo-l"><span style="color: #E69875;">    private readonly</span><span> metadataScanner</span><span style="color: #E69875;">:</span><span style="color: #7FBBB3;"> MetadataScanner</span><span>,</span></span>
<span class="giallo-l"><span style="color: #E69875;">    private readonly</span><span> reflector</span><span style="color: #E69875;">:</span><span style="color: #7FBBB3;"> Reflector</span><span>,</span></span>
<span class="giallo-l"><span style="color: #E69875;">    private readonly</span><span> discoveryService</span><span style="color: #E69875;">:</span><span style="color: #7FBBB3;"> DiscoveryService</span><span>,</span></span>
<span class="giallo-l"><span>  ) {}</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: #E69875;">  private</span><span> subject</span><span style="color: #E69875;">:</span><span style="color: #7FBBB3;"> Subject</span><span>&lt;</span><span style="color: #7FBBB3;">void</span><span>&gt;</span><span style="color: #E69875;"> =</span><span style="color: #E67E80;"> new</span><span style="color: #A7C080;"> Subject</span><span>();</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: #859289;font-style: italic;">  // subscribe to the shutdown in main.ts</span></span>
<span class="giallo-l"><span style="color: #A7C080;">  subscribe</span><span>(</span><span style="color: #A7C080;">callback</span><span style="color: #E69875;">:</span><span> ()</span><span style="color: #E69875;"> =&gt;</span><span style="color: #7FBBB3;"> void</span><span>)</span><span style="color: #E69875;">:</span><span style="color: #7FBBB3;"> void</span><span> {</span></span>
<span class="giallo-l"><span style="color: #D699B6;">    this</span><span style="color: #859289;">.</span><span>subject</span><span style="color: #859289;">.</span><span style="color: #A7C080;">subscribe</span><span>(()</span><span style="color: #E69875;"> =&gt;</span><span style="color: #A7C080;"> callback</span><span>());</span></span>
<span class="giallo-l"><span>  }</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: #A7C080;">  validate</span><span>()</span><span style="color: #E69875;">:</span><span style="color: #7FBBB3;"> void</span><span> {</span></span>
<span class="giallo-l"><span style="color: #E69875;">    const</span><span> controllers</span><span style="color: #E69875;">:</span><span style="color: #7FBBB3;"> InstanceWrapper</span><span>&lt;</span><span style="color: #7FBBB3;">Controller</span><span>&gt;[]</span><span style="color: #E69875;"> =</span><span style="color: #D699B6;"> this</span><span style="color: #859289;">.</span><span>discoveryService</span><span style="color: #859289;">.</span><span style="color: #A7C080;">getControllers</span><span>();</span></span>
<span class="giallo-l"><span style="color: #859289;font-style: italic;">    // validate each controller</span></span>
<span class="giallo-l"><span>    controllers</span><span style="color: #859289;">.</span><span style="color: #A7C080;">forEach</span><span>((controller)</span><span style="color: #E69875;"> =&gt;</span><span> {</span></span>
<span class="giallo-l"><span style="color: #E69875;">      const</span><span> instance</span><span style="color: #E69875;"> =</span><span> controller</span><span style="color: #859289;">.</span><span>instance;</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: #859289;font-style: italic;">      // get all methods / endpoints</span></span>
<span class="giallo-l"><span style="color: #E69875;">      const</span><span> proto</span><span style="color: #E69875;"> =</span><span> Object</span><span style="color: #859289;">.</span><span style="color: #A7C080;">getPrototypeOf</span><span>(instance);</span></span>
<span class="giallo-l"><span style="color: #E69875;">      const</span><span> methods</span><span style="color: #E69875;">:</span><span style="color: #7FBBB3;"> Type</span><span>&lt;</span><span style="color: #7FBBB3;">any</span><span>&gt;[]</span><span style="color: #E69875;"> =</span><span style="color: #D699B6;"> this</span><span style="color: #859289;">.</span><span>metadataScanner</span><span style="color: #859289;">.</span><span style="color: #A7C080;">scanFromPrototype</span><span>(instance, proto, (method)</span><span style="color: #E69875;"> =&gt;</span><span> {</span></span>
<span class="giallo-l"><span style="color: #E69875;">        const</span><span> instanceHandle</span><span style="color: #E69875;"> =</span><span> instance</span><span style="color: #859289;">.</span><span>constructor;</span></span>
<span class="giallo-l"><span style="color: #E69875;">        const</span><span> teamTag</span><span style="color: #E69875;"> =</span><span> reflector</span><span style="color: #859289;">.</span><span style="color: #A7C080;">getAllAndOverride</span><span>(</span><span style="color: #DBBC7F;">&#39;TEAM_TAG_METADATA_KEY&#39;</span><span>, [method, instanceHandle]); </span></span>
<span class="giallo-l"><span style="color: #E67E80;">	 if</span><span> (</span><span style="color: #E69875;">!</span><span>teamTag) {</span></span>
<span class="giallo-l"><span style="color: #D699B6;">          this</span><span style="color: #859289;">.</span><span>subject</span><span style="color: #859289;">.</span><span style="color: #A7C080;">next</span><span>();</span><span style="color: #859289;font-style: italic;"> // terminates the application</span></span>
<span class="giallo-l"><span>        } </span></span>
<span class="giallo-l"><span style="color: #E67E80;">        return</span><span> proto[method];</span></span>
<span class="giallo-l"><span>      });</span></span>
<span class="giallo-l"><span>    });</span></span>
<span class="giallo-l"><span>  }</span></span>
<span class="giallo-l"><span>}</span></span></code></pre>
<p>Next, we wire this logic on startup with NestJS application lifecycle hook.</p>
<pre class="giallo" style="color: #D3C6AA; background-color: #2D353B;"><code data-lang="javascript"><span class="giallo-l"><span style="color: #859289;font-style: italic;">// app.module</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: #83C092;">export</span><span style="color: #E67E80;"> class</span><span style="color: #7FBBB3;"> AppModule</span><span style="color: #E69875;"> implements</span><span style="color: #7FBBB3;"> OnApplicationBootstrap</span><span> {</span></span>
<span class="giallo-l"><span style="color: #E69875;">  constructor</span><span>(</span><span style="color: #E69875;">private readonly</span><span> moduleValidationService</span><span style="color: #E69875;">:</span><span style="color: #7FBBB3;"> ModuleValidationService</span><span>) {}</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span style="color: #A7C080;">  onApplicationBootstrap</span><span>() {</span></span>
<span class="giallo-l"><span style="color: #D699B6;">    this</span><span style="color: #859289;">.</span><span>moduleValidationService</span><span style="color: #859289;">.</span><span style="color: #A7C080;">validate</span><span>();</span></span>
<span class="giallo-l"><span>  }</span></span>
<span class="giallo-l"><span>}</span></span></code></pre>
<p>And then we complete the integration with an observable subscription to the module validation service callback to receive the termination signal.</p>
<pre class="giallo" style="color: #D3C6AA; background-color: #2D353B;"><code data-lang="javascript"><span class="giallo-l"><span style="color: #859289;font-style: italic;">// main.ts</span></span>
<span class="giallo-l"></span>
<span class="giallo-l"><span> app</span><span style="color: #859289;">.</span><span style="color: #A7C080;">get</span><span>(ModuleValidationService)</span><span style="color: #859289;">.</span><span style="color: #A7C080;">subscribe</span><span>(</span><span style="color: #A7C080;">terminateApp</span><span>(app) const</span><span style="color: #A7C080;"> terminateApp</span><span style="color: #E69875;"> =</span><span> (app</span><span style="color: #E69875;">:</span><span style="color: #7FBBB3;"> INestApplication</span><span>)</span><span style="color: #E69875;"> =&gt;</span><span> {</span></span>
<span class="giallo-l"><span style="color: #E67E80;">  return</span><span> ()</span><span style="color: #E69875;"> =&gt;</span><span> {</span></span>
<span class="giallo-l"><span>    app</span><span style="color: #859289;">.</span><span style="color: #A7C080;">close</span><span>();</span></span>
<span class="giallo-l"><span style="color: #E67E80;">    throw new</span><span style="color: #A7C080;"> ModuleValidationFailedError</span><span>();</span></span>
<span class="giallo-l"><span>  };};)</span></span></code></pre><h3 id="6-sentry-app-creating-a-sentry-alert-rule">6. Sentry App: Creating a Sentry Alert Rule</h3>
<p>The final piece of the puzzle is configuring Sentry to leverage the team tag we’re now including in our error events. We create a Sentry alert rule to match the team tag encapsulated within the Sentry event and configure destination Slack Channel connection details.</p>
<figure>
  <img src="image-sentry-config-screen.png" alt="Sentry config screenshot">
  <figcaption style="text-align: center">Sentry configuration screen.</figcaption>
</figure>
<h2 id="looking-ahead-extending-the-metadata-pattern">Looking Ahead: Extending the Metadata Pattern</h2>
<p>Our team-based alerting mechanism is just the beginning. This metadata-driven approach opens up possibilities for enhancing operational reliability and code quality across the board. For example:</p>
<ul>
<li><strong>Multi-Team Ownership:</strong> We could extend the model to allow multiple teams to co-own certain endpoints by stringifying metadata or using structured formats.</li>
<li><strong>Visualizing Module Topologies:</strong> By leveraging NestJS tools like <code>DiscoveryService</code> and <code>MetadataScanner</code>, we can create a visual interface for exploring endpoint ownership and dependencies.</li>
<li><strong>Proactive Quality Controls:</strong> The same metadata framework could enforce standards such as secure public endpoint exposure or domain validation.</li>
</ul>
<p>Our ultimate goal is to foster a tech ecosystem where shared components like the BFF empower teams with autonomy and accountability—without compromising collaboration or reliability.</p>
<p>Thank you for reading! We hope this post has sparked ideas for tackling shared ownership and alerting challenges in your own platforms. We’d love to hear how you’ve approached these problems in your tech stacks!</p>
</content>
        
    </entry>
    
        
    <entry xml:lang="en">
        <title>Evolving Quality: Levelling up automated end-to-end testing for microservice architectures</title>
        <published>2024-03-05T00:00:00+00:00</published>
        <updated>2024-03-05T00:00:00+00:00</updated>
        <author>
          <name>
            
              Endowus
            
          </name>
        </author>
        <link rel="alternate" type="text/html" href="/e2e-testing/"/>
        <id>/e2e-testing/</id>
        
        <summary type="html"><p>In this post, we’re going to share the story of how we tackled the tough task of testing microservices. We’ll talk about the roadblocks we hit with traditional end-to-end testing and how these challenges pushed us to come up with our own solution for test automation. You’ll get an inside look at the steps we took to build a test automation platform that lets us confidently release new code.</p></summary>
        
    </entry>
    
        
    <entry xml:lang="en">
        <title>Understanding and Resolving OOMKilled errors in JVM microservices</title>
        <published>2023-12-14T00:00:00+00:00</published>
        <updated>2023-12-14T00:00:00+00:00</updated>
        <author>
          <name>
            
              Endowus
            
          </name>
        </author>
        <link rel="alternate" type="text/html" href="/oomkilled/"/>
        <id>/oomkilled/</id>
        
        <summary type="html"><p>Have you ever faced the dreaded “OOMKilled” (Out Of Memory Killed)
message in your Kubernetes environments? If so, you’re not alone!</p>
<p>In this post, we’ll dive into a real-world scenario where some of our
backend services faced exactly this challenge. We’ll unpack what
happened, how it was investigated, and the lessons learned. We have
tried to keep it understandable even if you’re not a JVM or Kubernetes
expert. Feedback welcome!</p></summary>
        
    </entry>
    
        
    <entry xml:lang="en">
        <title>A story of 3 bets - Tech Values - Part 2</title>
        <published>2023-07-22T00:00:00+00:00</published>
        <updated>2023-07-22T00:00:00+00:00</updated>
        <author>
          <name>
            
              Endowus
            
          </name>
        </author>
        <link rel="alternate" type="text/html" href="/tech-values-part-2/"/>
        <id>/tech-values-part-2/</id>
        
        <summary type="html"><p>In an <a href="/tech-values/">earlier post</a>, we shared how we think about Engineering Values at Endowus and how they guide our decision making. In this post, we will share some instances where we applied these values in real scenarios.</p>
<p>By the way, we also covered this topic in a talk titled <a rel="noopener external" target="_blank" href="https://www.youtube.com/watch?v=dk7sbq3kiZA">“From Theory to Practice: How Engineering Values Inform Technology Decisions,”</a> at APIdays Singapore 2023. The complete talk can be <a rel="noopener external" target="_blank" href="https://www.youtube.com/watch?v=dk7sbq3kiZA">viewed on YouTube</a>.</p></summary>
        
    </entry>
    
        
    <entry xml:lang="en">
        <title>Life at Endowus as a Mobile Engineering Intern</title>
        <published>2023-06-01T00:00:00+00:00</published>
        <updated>2023-06-01T00:00:00+00:00</updated>
        <author>
          <name>
            
              Endowus
            
          </name>
        </author>
        <link rel="alternate" type="text/html" href="/mobile-intern-experience/"/>
        <id>/mobile-intern-experience/</id>
        
        <summary type="html"><p>Hello! I’m <a rel="noopener external" target="_blank" href="https://www.linkedin.com/in/amelia-tay-li-jia/">Amelia Tay</a>, a third-year undergraduate studying Information Systems at the Singapore Management University and I’ve spent my past 5 months with Endowus as a mobile engineering intern. Taking my last week here to reflect on these past few months has made me realise how pivotal of a role Endowus has played in my personal growth. So here I am, hoping to share with you how that came to be!</p></summary>
        
    </entry>
    
        
    <entry xml:lang="en">
        <title>Experimenting with ChatGPT for DevOps</title>
        <published>2023-04-07T00:00:00+00:00</published>
        <updated>2023-04-07T00:00:00+00:00</updated>
        <author>
          <name>
            
              Endowus
            
          </name>
        </author>
        <link rel="alternate" type="text/html" href="/chatgpt-for-devops/"/>
        <id>/chatgpt-for-devops/</id>
        
        <content type="html" xml:base="/chatgpt-for-devops/"><p>I had been working in the DevOps and Site Reliability space for roughly 5 years by the time that ChatGPT made it’s ground
shattering debut to the world, and it truly revolutionized how we all think about work and technology.
At Endowus, we strive to see how we can leverage modern technology to its fullest, and in this case, how ChatGPT can help
DevOps engineers optimize parts of their workflow.</p>
<p>We had all heard the rumours of ChatGPT being able to write a complete application from scratch. With a healthy level
of skepticism, I thought “it can’t <em>really</em> be that good”. So I decided to test it out and see for myself.</p>
<p>Initially, I started asking it to produce small scripts in Python and Bash, just to dip my toes in the OpenAI/ChatGPT waters.</p>
<p style="text-align: center;">
    <img src="gpt-python-sample.png" style="border: 0;" alt="ChatGPT writing an automated Python Script to perform memory checks on VMs.">
</p>
<p>The results were impressive to say the least! 🤯 Not only did it produce code with perfect syntax, but it also explained
the code to me and listed assumptions!</p>
<p>At the time, I had been working on deploying Airflow to an EKS Cluster using Helm via Terraform. This setup took roughly
3–5 days to get all squared away and tested. Curious how ChatGPT would perform, I asked it to write a configuration for this.
While it did produce the configuration, it wasn’t exactly what I was looking for or hoping for, in fact, it wasn’t really
even that close.</p>
<p>Next, I turned to its OpenAI sibling, the <a rel="noopener external" target="_blank" href="https://platform.openai.com/playground">Platform Playground</a>, and asked it the same question:</p>
<p style="text-align: center;">
    <img src="gpt-playground.png" alt="OpenAI’s Platform Playground tool generating Terraform Code for Apache 
Airflow deployment via Helm.">
</p>
<p>The OpenAI Platform Playground tool was such a game changer!
It produced much better code as it appears to have internet access where ChatGPT does not, and therefore can provide more
well-rounded configuration.</p>
<p>The configuration seen here that was output by the Playground tool was similar to the one I had configured a few days
prior manually, and it took this AI just a few seconds.
This can help reduce manual overhead of typing out the various fields, effectively, what it produced was a template
which I could then modify and use to deploy at a quicker rate.</p>
<p>I tried asking ChatGPT about specific infrastructure design and configuration options for setting up a theoretical metrics
system, a notoriously painful area to set up and maintain.
ChatGPT was able to give an answer in a matter of seconds that perfectly answered my questions and advised about best
practice where hours of reading Thanos docs and different resources online couldn’t give a definite way forward.</p>
<p style="text-align: center;">
    <img src="gpt-documentation.png" alt="Asking ChatGPT to summarize online documentation.">
</p>
<p>A few weeks later, I was troubleshooting and looking for a solution to an error that kept coming up in some logs for the
task that I was working on. It occurred to me that maybe I could optimize this troubleshooting process by seeing if ChatGPT had a solid resolution:</p>
<p style="text-align: center;">
    <img src="gpt-troubleshooting.png" alt="Asking ChatGPT a technical infrastructure design question.">
</p>
<p>While ChatGPT didn’t provide much new information about how to troubleshoot or resolve the error from what I had already
tried or found online, it can always help remind you to check something that you may have missed or forgotten. I’m sure
with different errors it may give more detailed responses and may even provide the specific solution to a given error.</p>
<p>This opens the door for so many, especially more junior engineers, to hone their troubleshooting and triaging skills
through ChatGPT. It may also change the entire flow for how we investigate various issues not only in CI/CD and
Infrastructure, but even for debugging code.</p>
<p>These two tools combined, ChatGPT and OpenAI Playground, have truly allowed me to improve and optimize my workflow.
They have unblocked me at various points over the last two months, either when I simply couldn’t find what I needed on Google or when I just needed a quick question answered.</p>
<h2 id="getting-the-most-out-of-chatgpt-for-your-workflow">Getting the most out of ChatGPT for your workflow</h2>
<ul>
<li><strong>Don’t tell ChatGPT your secrets:</strong> it’s important to be careful when you ask ChatGPT questions. Clean and remove
any proprietary information, personal information (PII) or credentials/application secrets. For example, if your prompt
contains any of this information, you can replace this sensitive information with other terms. Then if you use what
ChatGPT generated, replace it with the correct details locally.</li>
<li><strong>Specify Versions</strong>: if you want ChatGPT to generate configuration based on a Helm Chart (for example) that may have
multiple versions, I’ve found that it doesn’t always take the latest by default. It can be helpful to include the version
of the configuration you want in the request to get more accurate results.</li>
<li><strong>Trust but verify</strong>: as the quote goes “trust but verify”. You can trust that the output that ChatGPT is giving you
is good and is error free, but as with any code obtained online, you should always read through it yourself and ensure
you understand it before adding it to your project. Never assume that code generated by ChatGPT, no matter how amazing
or excellent, is production ready out of the box.</li>
<li><strong>Try the Playground</strong>: If ChatGPT isn’t giving the responses you want, you can also try using <a rel="noopener external" target="_blank" href="https://platform.openai.com/playground">OpenAI’s Playground</a>. It’s important to add that there is a free trial for this product line and once the trial ends, it becomes paid.</li>
<li><strong>Give Examples</strong>: it can be helpful to provide more information when asking ChatGPT something, this can also be done
in the form of giving it an example. Instead of just asking <code>regex to find an ip address in a string</code>, you can instead ask <code>regex to find an ip address in a string, for example, in a string 1.1.1.1:0000 the regex should match 1.1.1.1</code> and it will perhaps generate a more exact response for your query.</li>
</ul>
<p style="text-align: center;">
    <img src="gpt-example.png" alt="Giving ChatGPT an example of the type of output you want.">
</p>
<p>ChatGPT offers plenty of ways and opportunities for engineers to increase their productivity and help unblock themselves
in ways that never existed before and can help optimize their workflow in certain aspects.</p>
<p>However, at the current version of ChatGPT, it is unlikely that DevOps Engineers or SRE’s are at risk for being replaced.
While it’s an impressive tool, it’s important to know how best to use and leverage it for the outcome you wish to achieve.</p>
</content>
        
    </entry>
    
        
    <entry xml:lang="en">
        <title>Unlocking Engineering Productivity Through Unlimited Isolated Test Environments</title>
        <published>2023-03-24T00:00:00+00:00</published>
        <updated>2023-03-24T00:00:00+00:00</updated>
        <author>
          <name>
            
              Endowus
            
          </name>
        </author>
        <link rel="alternate" type="text/html" href="/unlocking-engineering-productivity/"/>
        <id>/unlocking-engineering-productivity/</id>
        
        <summary type="html"><p>At Endowus, we believe that happy &amp; productive developers create products that customers love. Thus, we regularly invest in developer experience and in this post, we are sharing the story of one of our larger investments in this area. The story of how we increased our engineering productivity by building an internal platform that allows our developers to deploy and test our full digital wealth platform in isolated production-like environments.</p></summary>
        
    </entry>
    
        
    <entry xml:lang="en">
        <title>How Endowus engineers the right tech values</title>
        <published>2023-01-01T00:00:00+00:00</published>
        <updated>2023-01-01T00:00:00+00:00</updated>
        <author>
          <name>
            
              Endowus
            
          </name>
        </author>
        <link rel="alternate" type="text/html" href="/tech-values/"/>
        <id>/tech-values/</id>
        
        <summary type="html"><p>There have been many attempts at defining the culture of an organisation. Some claim culture stems from a company’s vision and mission. Others think it’s about perks and benefits.</p>
<p>We think the best articulation of what constitutes a company’s culture is found in Ben Horowitz’s book <a rel="noopener external" target="_blank" href="https://a16z.com/book/whatyoudo/"><em>“What You Do Is Who You Are”</em></a>, in which he writes:</p>
<blockquote>
<p>Your culture is how your company makes decisions when you are not there.
It’s the set of assumptions your employees use to resolve the problems they face every day.
It’s how they behave when no one is looking.</p>
</blockquote>
<p>This rubric of decision making can be captured as that which a company values. As individual contributors, and later as engineering leaders at many different companies, we’ve witnessed first-hand how engineering teams make choices and decisions that are influenced by these cultural values.</p>
<p>Every company, every department, and every team has values. Whether they realise it or not and whether they explicitly write them down or not - they exist and are the invisible force guiding employee behaviours and decision making.</p></summary>
        
    </entry>
    
</feed>
