<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Revolut Tech - Medium]]></title>
        <description><![CDATA[Revolut under the hood - Medium]]></description>
        <link>https://medium.com/revolut?source=rss----44c5ac415e14---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Revolut Tech - Medium</title>
            <link>https://medium.com/revolut?source=rss----44c5ac415e14---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 13 Nov 2026 14:31:26 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/revolut" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Building the Web that Moves Money: inside Web engineering at Revolut]]></title>
            <link>https://medium.com/revolut/building-the-web-that-moves-money-inside-web-engineering-at-revolut-3dd4fb78fbfa?source=rss----44c5ac415e14---4</link>
            <guid isPermaLink="false">https://medium.com/p/3dd4fb78fbfa</guid>
            <category><![CDATA[fintech]]></category>
            <category><![CDATA[web-development]]></category>
            <category><![CDATA[web-engineering]]></category>
            <dc:creator><![CDATA[Revolut]]></dc:creator>
            <pubDate>Thu, 16 Oct 2025 09:45:59 GMT</pubDate>
            <atom:updated>2026-02-20T09:40:28.829Z</atom:updated>
            <content:encoded><![CDATA[<h3>Building the Web that moves money: inside Web engineering at Revolut</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wkwrjCgCbj52xsr1M-fsvA.png" /></figure><h3>Introduction</h3><p>In today’s digital-first economy, the web is the critical touchpoint between people and their money. At Revolut, we embrace this reality in our approach to web engineering: focusing on not just building interfaces, but on creating seamless, secure, and future-proof financial experiences.</p><p>Our mission at Revolut is to redefine the global standard for digital finance. Every line of code, design decision, and feature we release serves this goal: to make finance more accessible and beautifully engineered.</p><p>These principles shape our culture, set a high standard for excellence, and require constant attention to detail. They also drive us to rethink modern web development tools and practices.</p><p>This article focuses on the principles that help us deliver secure, beautiful financial products worldwide, and explains how engineers are building their careers along the way.</p><h3>The scope of Revolut web products</h3><p>Two independent but complementary pillars represent the current stage of the Revolut web ecosystem.</p><p><strong>Customer-facing applications<br></strong>These are the digital gateways through which our customers and partners experience Revolut directly:</p><p><a href="http://revolut.com/">Main website and brand hub</a></p><p><a href="https://www.revolut.com/business/">Revolut Business platform</a></p><p><a href="https://exchange.revolut.com/">Revolut crypto exchange platform</a></p><p><a href="http://people.revolut.com/">Human Resources Management Systems (HRMS)</a> by<a href="http://revolut.com/"> Revolut</a></p><p>Each of these touchpoints is an individual part of a unified ecosystem offering a consistent <strong>product experience</strong> that is closely aligned with our mobile platform.</p><p><strong>Internal web products<br></strong>Alongside our external platforms, Revolut operates <strong>around a hundred internal web applications</strong>. These provide our employees, operations, and partners with tools for:</p><ul><li>Back-office management</li><li>Compliance and risk monitoring</li><li>Customer support services</li><li>Data and reporting systems</li><li>Operational efficiency at scale</li></ul><p>These internal tools are the driving force behind Revolut, ensuring customer-facing products remain fast, secure, and globally scalable</p><h3>Core principles of web engineering in Revolut</h3><p><em>Principles set the direction. Practices deliver the results.</em></p><p>Revolut’s web ecosystem is built on <strong>scale</strong> — nearly a hundred independent applications, all evolving to meet the demands of a fast-growing business. To keep pace, we run on <strong>small, empowered teams</strong> that take ownership of entire services, driving speed and accountability.</p><p>Our approach is guided by clear<a href="https://medium.com/revolut/under-the-hood-engineering-at-revolut-2dc183c04228"> <strong>engineering principles</strong></a> and shaped by modern agile practices, inspired by eXtreme Programming (XP). However, principles alone aren’t enough. We amplify them with <strong>critical practices</strong> that turn vision into execution and impact.</p><p>Together, these principles and practices form the framework that lets us <strong>ship fast, adapt to hyper-growth, and never compromise on quality</strong>.</p><p>Now, let’s look at how these ideas translate into <strong>real-world solutions.</strong></p><h3>Adapting to hyper-growth</h3><p><em>Build systems that can explain their own evolution.</em></p><p>As the business grows rapidly, so must our codebases and teams. Designing for change means designing for the future as well as the present. It’s structure that promotes scaling, not the other way around. In practice, this means:</p><ul><li><strong>Code should explicitly reflect current business needs </strong>— no more, no less.<a href="https://en.wikipedia.org/wiki/Domain-driven_design"> Domain-Driven Design (DDD)</a> helps us organise the code on the frontend in a way that keeps it aligned with the domain. While there are many books on this topic, almost all of them focus on backend implementation when it comes to ‌tactical patterns. We adapt these ideas for the frontend, applying bounded contexts, clear models, and explicit contracts to ensure that our codebase mirrors the business as it evolves. This way, the system itself becomes the most reliable source of truth, guiding both engineers and stakeholders.</li><li><strong>Treat types as executable documentation </strong>— every type in your codebase is a specification that guides developers, prevents errors, and accurately documents intent. Your types are not just safety nets. They’re the clearest, most reliable form of communication between engineers, designers, product owners, and machines. Design your types so that only correct, meaningful states are possible. We love to use type <a href="https://www.typescriptlang.org/docs/handbook/2/narrowing.html#the-never-type">NEVER</a> a lot, as it makes the invalid states unrepresentable. These “type guards” turn runtime errors into compile-time guarantees, ensuring that every path the code can take is intentional, safe, and self-documenting.</li><li><strong>Systems must fail loudly and clearly when new, unsupported states appear</strong> — unexpected inputs and impossible branches should trigger explicit, actionable errors rather than silent degradation. We enforce this by validating every external boundary (APIs, configs, feature flags) <strong>at runtime </strong>(we’ve bet on<a href="https://zod.dev/"> Zod</a>), aligning schemas with our TypeScript types. Hence, the same contract guards both compile time and production;</li><li><strong>The system itself should guide its future evolution</strong> — the architecture, components, designs, and type system of our web platform act as a map for growth. By embedding conventions, designing tokens, and sharing libraries that strictly rely on the centric UI-kit library, we ensure that every new feature aligns with established patterns, marking the path of least resistance for growth and evolution.</li></ul><h3>Never compromise on quality</h3><p><em>Build trust through correctness and clarity.</em></p><p>In financial products, <strong>quality </strong>is the foundation of customer trust. A bug, security gap, or broken flow can impact people’s money directly, leading to reputational and regulatory risks.</p><ul><li><strong>Make quality everyone’s responsibility.</strong> At Revolut, we don’t have dedicated QA engineers. Instead, we see this as a strength. Engineers design, build, test, and monitor their products themselves. They provide engineering expertise in writing code, and also in testing it. This tight ownership creates a direct feedback loop, fosters a culture of accountability, and ensures that quality is built in from the first line of code — not inspected at the end.</li><li><strong>Ensuring stability on components, features and product levels</strong>. Our testing approach is built around Playwright and Jest, including unit, functional and production e2e types of tests. It allows us to automate end-to-end scenarios confidently, ensuring that key user flows work as expected across browsers and platforms. This helps us catch regressions early and maintain stability without slowing down delivery;</li><li><strong>Sensitive production monitoring is a part of the quality loop. </strong>We complement the automated testing approach with a continuous production monitoring system, ensuring quality doesn’t stop at release. We identify, debug, and resolve production issues with<a href="http://sentry.io/"> <strong>Sentry</strong></a><strong>. </strong>Sensitive alerts notify our engineers of every issue occurring in production, enabling early detection and rapid response. They help us catch edge cases that slip past automated tests and manual validation, often before users notice or report them to customer support. This proactive approach is essential for maintaining trust and minimising impact, particularly in an environment where the engineering team is responsible for quality from start to finish.</li><li><strong>Fixing a bug is just the first step.</strong> Outstanding quality means using every issue as an opportunity to improve the system. We treat each bug as a chance to uncover gaps, strengthen our architecture, and prevent similar problems from happening again.</li></ul><p>Q<strong>uality concerns are always the top priority</strong>. They’re non-negotiable in code reviews, routine planning, and the shaping of engineering strategy. No matter the pressure, we never trade quality.</p><h3>Built to ship fast</h3><p><em>Speed comes from clarity, not shortcuts.</em></p><p>Velocity doesn’t mean skipping steps. It means eliminating friction and building systems that facilitate safe and rapid execution.</p><ul><li><strong>Types as the first line of testing</strong>. When the types mirror the business rules they become the immediate quality guards with the instant feedback, catching issues before tests even run. This accelerates development times and shortens the feedback loop. This makes iterations faster, reduces wasted cycles, and helps teams ship with confidence.</li><li><strong>Standards are the foundation of a true ecosystem</strong>. We simply don’t do custom styling in our projects. Together with the central design system team, we develop robust, resilient UI patterns. The patterns are translated into ready-to-use components stored inside a centralised rich Uikit library. This means that every new design decision becomes a standard available for every project.</li><li><strong>Developers should solve business needs, not fight code complexity</strong>. We reduce cognitive overhead, automate the boring parts, and let engineers focus on what matters. The frontend platform team builds technical solutions that simplify product development across Revolut. Our internal frameworks and tooling provide a ready-made foundation, so new services launch with everything needed for growth and maintenance.<br>In other words, we’ve built an <strong>orbital launch station</strong> for projects — allowing teams to start from orbit, not from the ground, and accelerating delivery and innovation across the company</li></ul><p>These principles are a shared compass for engineers. Revolut is all about innovations, but everything we build must support clarity, resilience, and confidence.</p><h3>🌟 Excellence through people</h3><p><em>World-class products can only be built by world-class people</em></p><p>Engineering principles are like cookbooks: they provide the instructions for creating reliable, scalable, and innovative products. But it’s <strong>people</strong> who bring those recipes to life.</p><ul><li><strong>Hire A-Players.</strong> A-Players are more than technically skilled. They’re impact-driven, adaptable to change, and raise the performance bar for everyone around them. At Revolut, our interviews place candidates in real-world scenarios, asking them to implement features standard to web engineers, the solutions they probably did this week in their current job. We don’t focus on cracking obscure JavaScript puzzles (like figuring out how <em>0.1 + 0.2 !== 0.3</em>), we don’t ask “How many golf balls fit in a school bus?”, we value the way of solving the practical tasks (think deeper), the ability to deliver solutions end-to-end (get it done), and the sense of perfection that drives excellence (deliver wow).</li><li><strong>Grow A-Players</strong>. Impact creates leaders. We foster engineers at Revolut who make an impact and empower their strengths. At any seniority.</li><li><strong>Empower A-Players. </strong>High performers need trust and ownership. With autonomy, they move faster, take responsibility, and lift the whole team.</li></ul><p>When put into practice, our principles shape a culture of <strong>clarity, ownership, and innovation</strong>. Company-wide patterns enable developers to move fast with confidence, collaborate seamlessly, and continuously raise the standard of web products. This culture isn’t accidental — it’s the natural result of aligning philosophy with execution. It enables Revolut to scale web engineering to hundreds of people, hundreds of products, dozens of teams, and millions of customers moving billions of dollars.</p><h3>Tech stack</h3><p><em>Legacy is valued. Progress is mandatory.</em></p><p>Revolut is now a well-established company with a history spanning more than 10 years. It was founded in 2015, just a year after React 0.12 was released. Over this time, we’ve accumulated a wide range of solutions, approaches, tools, experiments, and lessons learned.</p><p>We treat legacy with respect and don’t rush to rewrite entire products every time a new library version is released. Instead, we follow a structured process to ensure that dependencies are updated in a timely manner across all systems. When a new version of a core or widely used library is issued, we first upgrade it in a specific project. Once stability is proven, automation triggers update requests across all dependent products.</p><p>This layered approach keeps our stack unified, up to date, and — most importantly — adaptable to change.</p><p>Our tech stack is well known in the web engineering community, and we work to keep it <strong>clean, light, and modern</strong>. We run on the latest stable releases — never more than one major version behind — ensuring consistency and adaptability across all products.</p><ul><li>Typescript — latest stable (within 1 major version)</li><li>React, React Query — latest stable (within 1 major version)</li><li>Bitbucket for code storage</li><li>TeamCity for build management (CI/CD)</li><li>Rush and NX for monorepo management</li><li>Sentry platform for error tracking</li><li>Kubernetes for automated app deployment, scaling, and management</li></ul><p>For testing we use:</p><ul><li>Jest</li><li>Playwright</li><li>Storybook</li><li>React testing library</li></ul><h3>Engineering with purpose</h3><p>Our philosophy is to <strong>let the code reflect the business, let the system empower the team, and let mistakes guide the future.</strong> <br>We believe in people<strong>, and their ability to grow to shape the future.</strong> <br>But we also believe in balance — <strong>respecting the past, while building for what’s to come.</strong> These principles encapsulate the essence of web engineering at Revolut: confident, flexible, and driven to set new standards.</p><h3>Join Revolut</h3><p><em>Make an impact at a global scale</em></p><p>At Revolut, there’s always room for talent.<br>We don’t define your journey for you. Bring curiosity, drive, and ownership, and you’ll shape your own path while making an impact on a global scale.</p><p>Our ecosystem is a unique opportunity for web technology — built for massive scale, the highest quality standards, and the ambition to keep pace with a fast-growing business.</p><p>Want to shape what’s next in finance?</p><p><strong>See where you can make your impact at — </strong><a href="https://revolut.la/InsideWebRevolut"><strong>Revolut Careers</strong></a><a href="https://www.revolut.com/careers"><strong>.</strong></a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3dd4fb78fbfa" width="1" height="1" alt=""><hr><p><a href="https://medium.com/revolut/building-the-web-that-moves-money-inside-web-engineering-at-revolut-3dd4fb78fbfa">Building the Web that Moves Money: inside Web engineering at Revolut</a> was originally published in <a href="https://medium.com/revolut">Revolut Tech</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Reinventing Risk at Revolut]]></title>
            <link>https://medium.com/revolut/reinventing-risk-at-revolut-77e63c552503?source=rss----44c5ac415e14---4</link>
            <guid isPermaLink="false">https://medium.com/p/77e63c552503</guid>
            <category><![CDATA[knowledge-graph]]></category>
            <category><![CDATA[risk-management]]></category>
            <category><![CDATA[banking]]></category>
            <category><![CDATA[finance]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Konstantin Vasilev]]></dc:creator>
            <pubDate>Tue, 10 Dec 2024 07:46:23 GMT</pubDate>
            <atom:updated>2024-12-10T07:46:23.109Z</atom:updated>
            <content:encoded><![CDATA[<h4>Using probability, reasoning and AI</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nyCTaBZO9rK6nzqja_W6uA.png" /></figure><p>Revolut is known for its innovation in delivering “super-app” financial services to its customers. The success is evident not only through the rocketing user growth, but also via the ripple effects caused across the banking industry — traditional finance has to reinvent and evolve, or remain part of history, just like the internet search replaced the paper phonebook.</p><p>Behind this success stands a complex and impressive machinery, which is still piloted by the same founders, who also engineered the very first components and set the Revolut rocketship in motion.</p><p>This machine is mostly known for its software excellence, however, with any financial services comes the responsibility of navigating a complex net of regulations and risks. True to the company values, the Risk function at Revolut also innovates, leveraging the decades of banking experience of its leadership and adding the challenger way of thinking with the latest technology.</p><h3>What is Risk?</h3><p>One can find many definitions of risk, ranging from explanations suitable for children, “The possibility of something bad happening”, to advanced statistical definitions used in finance:</p><blockquote>Uncertainty of the return on investment, characterised by standard deviation, asymmetry and tails of the return distribution</blockquote><p>and the generic <a href="https://www.openriskmanual.org/wiki/ISO_31000">ISO 31000</a> definition:</p><blockquote>“The effect of uncertainty on objectives”</blockquote><p>In addition, both graduate textbooks and banking regulations define two general types of risk:</p><ul><li><strong>Financial</strong>: related to financial positions, such as: user deposits, lending, and investments in market instruments.</li><li><strong>Operational</strong>: related to failure of operations, such as: <a href="https://en.wikipedia.org/wiki/2024_CrowdStrike_incident">system outage</a>, fraud and natural disasters.</li></ul><p>Some of you might already sense the inevitable challenges in distinguishing the above definitions and risks. Events seldom happen in isolation and independently: a natural disaster can lead to system outages and market crashes, similarly fraudsters might use fake accounts to withdraw loans which would never be repaid. Finally, sometimes adverse events just happen simultaneously even without having a common cause.</p><h4>Problem statement</h4><p>Financial institutions estimate and manage risk in large, segregated departments specialising in their own risk type. For example, the operational risk department has teams specialising in fraud and technology failures. The market risk department has teams focusing on risks stemming from investments in bonds or derivatives. To make it even more complicated, model risk teams oversee the possibility of all of the other teams using inappropriate techniques in estimating their risks — leading to the risk of risk, or something like risk squared.</p><blockquote>The above approach leads to methodological silos of how risk is defined, monitored, and estimated. Although the underlying probability theory is the same, the financial and operational risk departments might have broadly inconsistent approaches and even diverge in their views of the same risk event or double count some of the risks. This prevents accurate view and management of risk on enterprise level.</blockquote><p>Note that this legacy banking set-up is not the result of some careful consideration of all possible risk types and the optimal way to oversee them consistently. It is simply the result of old banking, tree-like structures which kept appending new teams and systems for tackling the risks from new products and regulatory requirements.</p><h4>Solution requirements</h4><p>If we imagine a new type of Enterprise Risk Management Framework (ERMF), what features should it posses in order to solve the above problems?</p><ol><li><strong>Scientific</strong>: Use scientific principles as universal and reusable building blocks that can define any risk, but also be shared across multiple risks.</li><li><strong>Measurable &amp; Comparable</strong>: Enable calculation of a standardised set of metrics for each risk that can be compared and combined across risks.</li><li><strong>Hypothetical</strong>: Allow for risk to be defined as a hypothesis of something that could happen, even when no observed data is available.</li><li><strong>Data Driven</strong>: Link the risks to live data sources in order to enable monitoring against data of observed business events, with the ability to monitor for any events that are missing in the currently defined risks.</li><li><strong>Learning</strong>: Update the hypothesis (requirement <strong>3</strong>) based on the difference between expected and observed data.</li><li><strong>Explainable</strong>: Be able to explain and decompose any result of calculated metrics (requirement <strong>2</strong>) to all components that lead to its derivation, including data inputs and logical rules. The generated audit trail will be extremely valuable during regulatory, audit, and validation exercises.</li><li><strong>AI assisted</strong>: Enable interaction via natural language with the defined risks and the audit trail of the calculated metrics, we can call this functionality <strong>Risk to Text</strong>. Equally important is the ability to generate risks, from natural language descriptions such as regulations or expert reports, we can call this functionality <strong>Text to Risk</strong>.</li></ol><h3>Reinventing Risk</h3><h4>How NOT to do it</h4><p>Legacy risk frameworks often start from high-level, top-down definitions such as risk types, controls, materiality, risk appetite, and so on, without clarifying their relations or defining first the common ground concepts that they share.</p><p>Thus, you end up with a framework in which you have a lot of high-level concepts which seem to be expert knowledge, however, when you drill down you find most of them overlapping or even contradicting each other. The culmination of this approach is usually a tree structure called Risk Taxonomy of different risk types and subtypes, here is what the <a href="https://www.openriskmanual.org/wiki/Risk_Taxonomy">Open Risk Manual</a> says about issues with this approach:</p><blockquote>“Risk types in practice may exhibit significant overlap either due to poor definition or due to the fact that a complex Risk Event intrinsically maps to multiple risk types”</blockquote><h4>How to GET IT DONE</h4><p>A general principle is to define as little as possible concepts unless they are based on our scientific building blocks, or if I may quote a famous electric car enthusiast “The best part is no part”.</p><p>We will start from the bottom-up and if a more specific concept cannot be derived from the building block, then either our foundation is not good or the concept is not needed at all.</p><h4>Building blocks = events</h4><p>Our choice of building block is the event, with the following arguments:</p><ol><li>The natural way that risk is usually thought of and described by humans typically involves events: “The risk that in the event of …”. This increases the ability to satisfy requirement <strong>7</strong>, and the translation of risk to and from natural language.</li><li>Risk estimation is heavily reliant on probability theory for things like frequency and severity distributions, both of these are always in terms of specific events such as risk incidents that lead to losses. This is consistent with requirement <strong>1</strong> for using scientific building blocks.</li><li>Modern IT systems are to a large extent event-driven, in order to deliver live information on things like news and stock prices. By making our risk framework event-based, we are opening the door to seamless integration with the data sources and live monitoring of our risks, based on specific business events. This is consistent with requirement <strong>4</strong>.</li></ol><p>The way we will think about an event is quite general, in terms of the event leading to a transition between two states over a period of time:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/600/1*O-SY9az4D1A1z4mwkEo4bA.gif" /><figcaption>State transition due to an event with a given probability</figcaption></figure><p>The main benefit of this is that we can stack any number of events and states into a network/graph of things that may or may not happen, sounds a bit similar to risk, right?</p><p>The other benefit is on the scientific side, from having the ability to perform standardised calculations over such structure. This is essentially a state space (Markov chain) representation, which is widely used in statistics. This satisfies requirements <strong>1</strong>,<strong>2</strong>, and <strong>3</strong>.</p><h4>Risk = chain of events</h4><p>Jumping straight into a specific example, one of the risks that Revolut faces is related to a user’s balance becoming negative (money owed to Revolut) and subsequently becoming a financial loss if not repaid within a given timeframe. This risk is interesting from two aspects:</p><ol><li>The base risk population has the broad scope of all users with positive balance.</li><li>The loss of negative balances represents pure credit risk, however, some of these losses can arise due to fraud which is operational risk, hence we could have composite risk with both financial and operational risk events. For simplicity, we will include only the credit risk in the example, however the extension with additional events and states is analogous.</li></ol><p>The main states included in the risk are: positive balance (+Balance), negative balance (-Balance), and (Lost). Where each state is defined in terms of the characteristics of the user instances. Stacking the states and the transitioning events, we obtain the following chain of events:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9I-QlhMYa-0Pex1XyoKRQw.gif" /><figcaption>Negative balance risk</figcaption></figure><p>Transitions from a state to itself are also possible, like a +Balance staying such, but also some states can be completely absorbing like a Lost balance always remaining such. Finally, above in red, we introduce the notion of loss rate which indicates what proportion of money can be lost in a given state. For simplicity, we assume 100% loss rate only in the lost state, however, in reality there could also be losses associated with the -Balance state, for example, due to collection costs from the negative balances.</p><p>Now that we have a solid and scientific building block, let’s start making structures with it. The above chain of events is our first example of a specific risk and generalising the definition, we end up with the following single sentence:</p><blockquote><strong>Risk</strong>: The potential of <strong>financial loss</strong>, as a result of a chain of <strong>events</strong>, over a given <strong>time horizon</strong></blockquote><p>Note that apart from defining risk as a chain of events, we also require it to be linked to a tangible financial loss and have a strict time dimension. This will help satisfy requirement <strong>2</strong>, by being able to calculate risk metrics. The financial loss need not necessarily be in absolute terms, and it could be in relative terms as opportunity cost relative to an objective, which is consistent with the <a href="https://www.openriskmanual.org/wiki/ISO_31000">ISO 31000</a> definition of risk.</p><h4>Metrics</h4><p>A main advantage of formal risk definition is that it enables the calculation of a standardised set of metrics for every risk. Back to the negative balance risk example, let’s set values for the main parameters of the risk, which can be considered ex-ante expert estimates:</p><ul><li>Amount of +Balances: 100£</li><li>Probabilities of events: <br>- P(stay +): 90%<br>- P(become -): 10%<br>- P(become +): 20%<br>- P(become lost): 80%<br>- P(stay lost): 100%</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6ZayNjpzz5c36eJ8ki0hQw.gif" /><figcaption>Negative balances risk; with initialised expectations in the form of probabilities and amount at risk</figcaption></figure><h4>Maximum loss</h4><p>The first and most trivial metric is worst case scenario loss, knowing that all probabilities above are just ex-ante expectations one can ask: what is the most I could lose in the worst case scenario? In our simplified example, this will happen if all £100 of positive balances become negative and then lost, leading to a loss of £100 * 100% = £100.</p><h4>Expected loss</h4><p>Now let’s try a more sophisticated metric in the form of <a href="https://en.wikipedia.org/wiki/Expected_loss#:~:text=Expected%20loss%20is%20the%20sum,%2C%20commercial%20lending%2C%20etc.)">Expected Losses</a> (EL or ECL). For this, we will need to define a time horizon, e.g. whether we form our expectation over 1 or 10 periods, and we will need to iterate over our ex-ante expectations. To keep the example simple let’s consider 2 period horizon, and starting from the +Balance state, we have the following paths at 2 period distance, where the cumulative probability along a path is obtained by multiplying the individual probabilities:</p><ul><li>[+Balance → -Balance → <strong>Lost</strong>]: P = 10% * 80% = <strong>8%</strong></li><li>[+Balance → -Balance → +Balance]: P = 10% * 20% = 2%</li><li>[+Balance → +Balance → -Balance]: P = 90% * 10% = 9%</li><li>[+Balance → +Balance → +Balance]: P = 90% * 90% = 81%</li></ul><p>In this simplified example, we can calculate <a href="https://en.wikipedia.org/wiki/Expected_loss#:~:text=Expected%20loss%20is%20the%20sum,%2C%20commercial%20lending%2C%20etc.)">EL</a> only by considering the paths which end state have a loss rate, since all others will be multiplied by 0. This is the path ending with <strong>Lost</strong> state, with <strong>8%</strong> probability and 100% loss rate, leading to EL = £100 * 8% * 100% = £8. Note the big difference between the maximum(£100) and the expected loss (£8) due to the probability weighting of the events in the risk.</p><p>Generalising further, the risk definition essentially gives us a generator of discrete loss distributions for any time horizon at which we want to form our expectations. These distributions are the backbone of risk theory and estimation, used even in <a href="https://www.bis.org/bcbs/irbriskweight.pdf">bank regulations</a> for calculating capital requirements. The maximum, expected, and unexpected losses are just one of the many generic metrics that can be calculated from such distributions which satisfy requirement <strong>2</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*zZJjN8CkHpLCdhRTVN4OBw.gif" /><figcaption>Normal loss distribution from chain of binary events (source: 3 Blue 1 Brown)</figcaption></figure><p>Fellow nerds, might have recognised the embedded <a href="https://math.libretexts.org/Bookshelves/Applied_Mathematics/Applied_Finite_Mathematics_(Sekhon_and_Bloom)/10%3A_Markov_Chains/10.01%3A_Introduction_to_Markov_Chains">Markov chain</a> in our Risk definition. This automatically unlocks the utilisation of tonnes of scientific knowledge, from things like efficiently computing n-period transitions via matrix multiplication, to advanced topics like: <a href="https://en.wikipedia.org/wiki/Bayesian_inference">Bayesian inference</a>, time inhomogeneous probabilities and <a href="https://mitpress.mit.edu/9780262013192/probabilistic-graphical-models/">probabilistic graphical models</a>.</p><h4>Observed loss</h4><p>In addition to enabling the calculation of standardised ex-ante expectations about what could happen, our risk definition also enables the monitoring of actual live events and calculation of ex-post observed metrics. This means that we can form subjective expectations before we even launch a given product. However, once data is available, we can calculate losses and use this to update our initial expectations. This satisfies ‌learning requirement <strong>5</strong> and is dependent on having an implementation that links our abstract states to data sources (requirement <strong>4</strong>). We will talk about this in the next section.</p><h3>Sounds nice, but how would you actually do it?</h3><p>The implementation requirements are becoming non-trivial, taking into account the needs of: defining high-level abstractions, linking them to data, performing efficiently recursive calculations over a large graph of rules, while preserving explainability and audit trail of the derived facts.</p><h4>Reasoning</h4><p>Semantic reasoning is the “inference of logical consequences from a set of asserted facts” (<a href="https://en.wikipedia.org/wiki/Semantic_reasoner">Semantic Reasoner</a>). It became initially popular with the idea of Semantic Web in the early 2000s, representing the evolution of the internet from linked pages (Web 1.0), to linked apps (Web 2.0), and linked data (Web 3.0). Reasoners enjoy a new surge of popularity together with the AI hype, due to their interaction with language models in terms of adding explainability and reducing hallucination.</p><p>For our demo, we used <a href="https://prometheux.ai/">Prometheux</a>, an explainable AI platform for big data and distributed processing, which we used as our reasoning layer. Prometheux’s <a href="https://prometheux.ai/#research">research</a>-backed platform abstracts away our various data sources, letting us iterate quickly on a virtual knowledge graph of our data and business rules, instead of writing complex database queries or migrating data.</p><p>While other knowledge graph technologies require representing relationships as triplets consisting of Subject, Predicate, Object, (e.g. is_son(Child,Father)), Prometheux is capable of representing more complex rules based on n-tuples.</p><h4>Methodology implementation</h4><p>Using Prometheux, we start by implementing our new risk methodology, first specifying data models of our main concepts: State, Event, Risk. The code below defines:</p><ol><li>State: something having ID and loss rate.</li><li>Event: something consisting of start and end State and a probability.</li><li>Risk: collection of Events.</li></ol><pre>@model(&quot;state&quot;,&quot;[&#39;State(ID):string&#39;, &#39;LossRate:double&#39;]&quot;).<br><br>@model(&quot;event&quot;,&quot;[&#39;Event(ID):string&#39;, &#39;StartState:state&#39;, &#39;EndState:state&#39;, &#39;Probability:double&#39;]&quot;).<br><br>@model(&quot;risk&quot;,&quot;[&#39;Risk(ID):string&#39;, &#39;Events:[event]&#39;]&quot;).</pre><p>Next, we proceed by adding the first logical rules, for a given set of parameters: [Risk, StartState, NumPeriods], we want to calculate all paths of NumPeriods distance from the StartState and their cumulative probability. The first rule initialises the path at the StartState, with 0 periods and a probability of 1, with the parameters being provided in curly brackets {}:</p><pre>risk_path_probability(Risk, StartState, StartState, NumPeriods, Events, Probability) :-<br>  risk(Risk, Events),<br>  NumPeriods = 0,<br>  StartState = ${start_state},<br>  Probability=1.0.</pre><p>The second rule defines a recursion that traverses events within the specified Risk, until the NumPeriods distance is reached, accumulating the probability of every new event along the path:</p><pre>risk_path_probability(Risk, StartState, EndStateNew, NumPeriodsNew, Events, ProbabilityNew) :-<br>  risk_path_probability(Risk, StartState, EndStateOld, NumPeriodsOld, Events, ProbabilityOld),<br>  event(Event, EndStateOld, EndStateNew, ProbabilityEvent), <br>  NumPeriodsNew = NumPeriodsOld + 1,<br>  ProbabilityNew = ProbabilityOld * ProbabilityEvent,<br>  IsEventOfRiskInstance = collections:contains(Events, Event), IsEventOfRiskInstance=#T,<br>  NumPeriodsNew &lt;= ${num_periods}.</pre><p>Finally, we define predicates that store the end probability at NumPeriods distance and one that aggregates all paths that end in the same state, to get end-state probabilities:</p><pre>risk_path_probability_end(Risk, StartState, NumPeriods, EndState, Probability) :-<br>  risk_path_probability(Risk, StartState, EndState, NumPeriods, Events, Probability),<br>  NumPeriods = ${num_periods}.<br><br>risk_state_probability_end(Risk, StartState, NumPeriods, EndState, ProbabilityState) :-<br>  risk_path_probability_end(Risk, StartState, NumPeriods, EndState, ProbabilityPath),<br>  ProbabilityState = msum(ProbabilityPath).</pre><h4>Risk register</h4><p>Now that we have data models and some logic, let’s define our negative balances risk with it. In the negative balances risk, the main abstraction is the user with their Balance property. Note that the object instances used in the risk have to comply with the earlier defined data models. One of the main advantages of using a reasoner, is that upon compilation it verifies the logical consistency amongst rules and data without allowing any contradictions.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6ZayNjpzz5c36eJ8ki0hQw.gif" /><figcaption>Negative balances risk; with initialised expectations in the form of probabilities and amount at risk</figcaption></figure><pre>state(“Positive balance”, 0.0).<br>state(“Negative balance”, 0.0).<br>state(“Lost”, 1.0).<br><br>event(&quot;Become negative&quot;, &quot;Positive balance&quot;, &quot;Negative balance&quot;, 0.1).<br>event(&quot;Stay positive&quot;, &quot;Positive balance&quot;, &quot;Positive balance&quot;, 0.9).<br>event(&quot;Recovery&quot;, &quot;Negative balance&quot;, &quot;Positive balance&quot;, 0.2).<br>event(&quot;Loss&quot;, &quot;Negative balance&quot;, &quot;Lost&quot;, 0.8).<br>event(&quot;Stay lost&quot;, &quot;Lost&quot;, &quot;Lost&quot;, 1.0).<br><br>risk(<br>    &quot;Negative Balances Risk&quot;,<br>    [&quot;Become negative&quot;, &quot;Stay positive&quot;, &quot;Recovery&quot;, &quot;Loss&quot;, &quot;Stay lost&quot;]<br>).</pre><h4>Data driven</h4><p>Requirement <strong>4</strong> is for our methodology to be data driven. The above ex-ante definitions are already enough to calculate metrics like expected losses, however, without an easy way to link to data sources we can’t calculate ex-post observed metrics and update our initial beliefs, which is part of the learning requirement <strong>5</strong>.</p><p>Fortunately, our reasoner allows us to directly bind predicates to data sources, such as: CSV, DB table/query, API. This is as simple as a single line pointing the predicate “user” to the data source, and of course, the data source is required to match the data model of the predicate:</p><pre>@input(&quot;user&quot;).<br>@bind(&quot;user&quot;,&quot;csv useHeaders=true&quot;,&quot;disk/negative balances/data&quot;,&quot;users.csv&quot;).</pre><p>In our case, the user abstraction is something with ID, Revolut entity ID, Balance, and Date. And we can imagine it being linked to a daily data source with all Revolut users and their balances. In this case, we’ve created an artificial dataset of user balance time series. Based on user data, we can derive our Risk specific states, depending on whether the balance is positive, negative, or if there has been a loss event:</p><pre>user_state(State, User, Balance, Date) :-<br>  user(User, EntityId, Balance, Date),<br>  not loss_event(User, EntityId, Balance, Date),<br>  Balance &gt;= 0,<br>  State = “Positive balance”.<br><br>user_state(State, User, Balance, Date) :-<br>  user(User, EntityId, Balance, Date),<br>  not loss_event(User, EntityId, Balance, Date),<br>  Balance &lt; 0,<br>  State = &quot;Negative balance&quot;.<br><br>user_state(State, User, Balance, Date) :-<br>  user(User, EntityId, Balance, Date),<br>  loss_event(User, EntityId, Balance, EventDate),<br>  Date &gt;= EventDate,<br>  State = &quot;Lost&quot;.</pre><h4>Risk calculation</h4><p>Before we trigger the calculation, let’s see how the reasoner querying works. This is as simple as requesting which predicate to be outputted, then all dependencies are evaluated including fetching from the data sources.</p><p>First we initiate the parameters which in our case is the starting state, date, and number of periods. Then we output the predicates. In this case let’s explore the artificial data, and see the user states at the start date and the end date at 11 days distance.</p><pre>@param(&quot;start_state&quot;, &quot;Positive balance&quot;).<br>@param(&quot;start_date&quot;, &quot;2024-12-01&quot;).<br>@param(&quot;num_periods&quot;, 11).<br><br>@output(&quot;risk_state_start&quot;).<br>@output(&quot;risk_state_end&quot;).</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*34rkM2SyIeE7ctIFmpFIhw.png" /><figcaption>Start and end state data for a given Risk</figcaption></figure><p>Now let’s output a predicate which defines how ex-ante expected loss is calculated — this will immediately trigger the materialisation=calculation of all dependencies. In this case, we can calculate expectations purely out of the risk definition and ex-ante probabilities, before we even have any actual data.</p><p>First, let’s output the end state probabilities at 11 periods distance from the starting state. Then we output the expected loss, which in this simplified case is dependent only on the Lost end state and its probability = 0.538 (all other end states have 0 loss rate), hence the expected loss of each user is proportional to their current balance ExpectedLoss = Balance*0.538</p><pre>@output(&quot;risk_state_probability_end&quot;).<br>@output(&quot;risk_expected_loss&quot;).</pre><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*i7Fl-s7IvbJXQrTgHBnMGA.png" /><figcaption>Ex-ante expected end state probability and expected loss on the start state population</figcaption></figure><p>Finally, using the end-state data we can calculate the observed losses, which closes the feedback loop and enables requirement <strong>5</strong> for learning based on expected vs actual observations:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/490/1*3NqGvjowW00IZg3EnQbzzQ.png" /><figcaption>Observed loss</figcaption></figure><h4>Generalisations</h4><p>Note that so far we’ve kept the example simple on purpose, however, the implemented methodology is general enough to accommodate more complicated cases:</p><ul><li>Apply calculations for a given Risk on a population with multiple states, e.g. if we want to calculate expected losses for the entire population of users including both those currently in negative and positive balance states.</li><li>Calculate metrics for different time horizons — parameterised by the number of periods.</li><li>Calculate multiple Risks for a given abstraction, e.g. a user can have both negative balances risk and credit risk if it possesses a credit product.</li></ul><h4>Explainable audit trail</h4><p>So far, there is nothing that effectively differs from an alternative Python implementation and which satisfies requirement <strong>6</strong> for explainability.</p><p>However, our implementation choice via reasoner keeps track of logic rules and their materialised results called facts inside a knowledge graph, which can provide explanation and audit trail (proof tree) of every derived fact. Let’s take, for example, one particular calculated fact and ask for its explanation:</p><pre>explain(fact=&quot;risk_expected_loss(B,8.07)&quot;)</pre><p>The above fact is based on the predicate risk_expected_loss with the arguments: user B in our population and calculated expected loss value of 8.07, but how was this derived?</p><p>By default, the reasoner provides a proof tree of all predicates that were used to derive the fact selected for explanation. The proof tree is described in code but can also be visualised as a graph:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/917/1*7voHBuwF6eqfQgf6Ib6o1A.png" /><figcaption>Proof tree generated by fact explanation</figcaption></figure><p>In the proof above, we can clearly see that the fact risk_expected_loss(B,8.07) was derived by the fact risk_expected_loss_rate(0.538) and the start state of the user, with the latter being further derived from a positive balance state of a user of Revolut Ltd.</p><p>The explain functionality of the reasoner satisfies requirement <strong>6</strong> which is extremely valuable in the financial sector where audit trail and logical transparency is of very high priority. In the risk domain, there are further benefits from explainability in delivering clear and objective methodology when assessing and controlling the risks of the business teams.</p><p>What if we want to deliver this explainability to the non-technical user and avoid code and predicates in the proof tree? Fortunately, the structured explanations are easily translated to natural language (NL) by a large language model (LLM).</p><h3>Risk xAI</h3><p>As I’ve <a href="https://towardsdatascience.com/the-struggle-of-artificially-imitated-intelligence-in-specialist-domains-6e63a4e0ebfc">previously discussed</a>, LLMs perform pattern matching and prediction of the most likely sequence of words without having any semantic understanding of the concepts behind them. This often leads to “hallucination” which can be fun in artistic domains but unacceptable in scientific ones.</p><p>Fortunately, our use of semantic reasoner enables an architecture that solves exactly this problem. The benefit of the reasoner is that it ensures 100% consistency and audit trail, with the drawback of the latter being in the form of machine code which is not human friendly, especially at scale. In combination, the Reasoner and LLM prove to be a surprisingly good tandem.</p><p>The <strong>Reasoner</strong> can handle:</p><ol><li><strong>Data models</strong> and <strong>Rules</strong> of our Risk risk domain compiled in a fully consistent <strong>Ontology</strong> (onto-logos from Greek meaning the study of existence, in our case the existence of Risk).</li><li>Binding of <strong>Data</strong> sources which can be used as required <strong>Inputs</strong> in order to derive requested <strong>Outputs</strong> following our Ontology. This process is called reasoning and is used to derive new <strong>Facts</strong>.</li><li><strong>Explanation</strong> of any Fact via <strong>Proof tree</strong>.</li></ol><p>Similar to the popular use of RAG (Retrieval-Augmented-Generation) for providing additional information to the LLM, we can store our domain <strong>Ontology</strong> and any <strong>Proof tree</strong> in a <strong>Vector DB</strong> and make this information available to the <strong>LLM</strong>.</p><p>The <strong>LLM</strong> can handle:</p><ol><li>Show <strong>NL</strong> (natural language) description of the individual <strong>Rules</strong>, e.g. “How is expected loss calculated?”.</li><li>Show <strong>NL</strong> description of the entire <strong>Ontology,</strong> including shared concepts between rules, e.g. “What is Risk and how it is defined?”.</li><li><strong>Explain</strong> any fact by translating the <strong>Proof tree</strong>, e.g. “How was the expected loss of x£ derived?”.</li><li>Have a <strong>NL</strong> chat around all of the above: facts, rules, explanations.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nz8FxTi3cqbtxCTmx2nECw.png" /><figcaption>Reasoner-LLM interactions</figcaption></figure><p>The architecture above satisfies requirement <strong>7</strong>, as we can harness the power of the reasoner and access it via human friendly NL. Now let’s see how it works in practice.</p><h4>Natural language explanation</h4><p>Using the LLM, we can generate a NL description of the fact risk_expected_loss(B,8.07)which proof tree and diagram we saw previously:</p><pre>User B has a positive balance of 15 with Revolut Ltd as of 2024-12-01.<br>The expected loss for user B is 8.07, with a loss rate of 0.538</pre><p>Note that the association with Revolut Ltd, is not something directly available in the fact on which the explanation was requested, this was correctly inferred by the LLM through the knowledge of the proof tree and the complete ontology from which the association with Revolut Ltd is visible.</p><p>Some might say “I can give the LLM python code and function results and it can also generate explanation” — yes, you can, but here the reasoner has the responsibility of providing 100% accurate explanation while the LLM has the task of “prettifying” this explanation to NL. The resulting combination is much more robust than any standalone LLM explanation.</p><h4>Risk to Text</h4><p>Proof tree explanation is just one of the surprisingly good interactions between reasoner and LLM. Even before any facts and explanations are generated, we can easily generate NL descriptions of the logic that we have implemented and the risk instances that we have added to it. Let’s look at the NL description of the predicate risk_path_probability:</p><pre>The probability of encountering risk when transitioning from a start state<br>to a new end state over a given number of periods is calculated<br>by multiplying the probability of encountering risk<br>in the previous period by the probability of the event causing the state transition,<br>provided the event is a risk instance and the total number<br>of periods does not exceed a specified limit.</pre><p>Just think of the current development and documentation process of such rules, in which risk managers have to provide business specifications and developers have to implement, while also writing an elaborate documentation. Here, essentially we have logic which can be self-describing and documenting.</p><h4>Auto doc and policy</h4><p>Let’s see ‌the LLM generated NL description of our entire ontology. You can imagine a use case of this functionality in automatically generating and updating documentations and internal policies and procedures. All of our examples are with minimal promt engineering meaning that there is room for a lot of customisation and improvement:</p><pre>The ontology defines a framework for assessing risk and loss in financial states through various predicates and rules. The key predicates and their roles are:<br>1. **state**: Represents financial states with an associated loss rate. Used to define states like “Positive balance”, “Negative balance”, and “Lost” with specific loss rates.<br>2. **event**: Describes transitions between states with associated probabilities. Events like “Become negative”, “Stay positive”, “Recovery”, “Loss”, and “Stay lost” define how states change over time.<br>3. **risk**: Represents a collection of events that define a specific risk, such as “Negative Balances Risk”, which includes events affecting balance states.<br>4. **user_state**: Indicates the state of a user’s balance at a given date. It determines if a user is in a “Positive balance”, “Negative balance”, or “Lost” state based on their balance and loss events.<br>5. **risk_state_start**: Marks the beginning of a risk state for a group with a specific amount and date. It is derived from the user state at the start date.<br>6. **risk_state_end**: Marks the end of a risk state for a group with a specific amount and date. It is derived from the user state at the end date.<br>7. **risk_path_probability**: Represents the probability of encountering a risk when transitioning between states over a number of periods, considering the occurrence of events.<br>8. **risk_path_probability_end**: Represents the probability of transitioning from a start state to an end state over a specified number of periods, summarizing the risk path probabilities.<br>9. **risk_state_probability_end**: Represents the probability of a risk transitioning from a start state to an end state over a number of periods. It aggregates the probabilities from risk path probabilities.<br>10. **risk_state_expected_loss_rate**: Represents the expected loss rate for an end state. It is calculated by multiplying the loss rate of the state by the probability of reaching that state.<br>11. **risk_expected_loss_rate**: Represents the overall anticipated loss rate associated with a specific risk. It sums the expected loss rates of all states.<br>12. **risk_expected_loss**: Represents the expected loss for a group, calculated by multiplying the expected loss rate by the amount at the start.<br>13. **count_state_start**: Represents the count of a state at the start. It is derived from the number of groups starting in that state.<br>14. **count_state_end**: Represents the count of a state at the end. It is derived from the number of groups ending in that state.<br>15. **risk_observed_state_probability**: Represents the observed probability of a risk ending in a specific state. It is calculated by dividing the count of the state at the end by the count at the start.<br>16. **risk_observed_loss**: Represents the observed loss for a group at the end state. It is calculated by multiplying the end amount by the loss rate of the end state.<br>The predicates link through rules to form a cohesive risk assessment model. Events define state transitions, which are grouped into risks. User states determine initial and final risk states, which are used to calculate probabilities and loss rates. The model aggregates these calculations to determine expected and observed losses and probabilities, providing a comprehensive understanding of financial risks and their impacts.</pre><h4>AI assistant</h4><p>Combining the ontology together with all of the reasoner — LLM interactions above, we can unleash the full potential by creating our own AI assistant with which we can chat about the different rules, how they work, but also how specific results were derived:</p><pre>chat(&quot;What are the risks associated with users?&quot;)</pre><p>Generates the following response:</p><pre>The risks associated with users include transitioning from<br>a &quot;Positive balance&quot; to a &quot;Negative balance&quot; with a probability of 0.1,<br>transitioning from a &quot;Negative balance&quot; to a &quot;Lost&quot; state with a probability of 0.8,<br>and remaining in a &quot;Lost&quot; state with a probability of 1.0.<br>The &quot;Lost&quot; state has a level of loss of 1.0, indicating a complete loss.<br>Additionally, users may experience &quot;Recovery&quot; from a &quot;Negative balance&quot;<br>to a &quot;Positive balance&quot; with a probability of 0.2,<br>and &quot;Stay positive&quot; where the &quot;Positive balance&quot; remains unchanged with a probability of 0.9.<br>The expected loss for a user is calculated based on these transitions and their associated probabilities.</pre><h4>Text to Risk and challenges ahead</h4><p>So far we implemented flexible and scientifically grounded methodology and with the help of LLM, delivered a user-friendly interaction without sacrificing accuracy and objectivity.</p><p>However, if we are to fully automate the process, we ought to improve the initial step of manually coding our specific risk instances. One way to do this is by using the LLM as software developer which has to follow both correct syntax but also the codified risk instances have to be compatible with the generic risk ontology. In that case, we can think of the ontology as a schema which provides a template for the LLM for what constitutes a valid risk. The success of the LLM can then be tested via compiling the existing ontology and the newly generated risk by the reasoner.</p><p>Our concept workflow is the following:</p><ol><li><strong>Risk manager</strong>: describes new risk and references data sources where its events can be observed.</li><li><strong>LLM</strong>: generates code for the new risk instance including the chain of events and possible states, as well as bindings to the data sources.</li><li><strong>Reasoner</strong>: adds the new risk to the existing ontology and compiles it. If compilation is successful, the risk is sent to the risk manager for review. If not, it is sent back to the LLM for correction.</li></ol><p>The above is a separate project by itself and is similar to the emerging topic of <a href="https://arxiv.org/html/2312.13010v2">AI agents</a> for code generation and iterative testing. Nevertheless, this is just another highlight of the vast potential for future development that is made possible by our scientific approach to Risk.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=77e63c552503" width="1" height="1" alt=""><hr><p><a href="https://medium.com/revolut/reinventing-risk-at-revolut-77e63c552503">Reinventing Risk at Revolut</a> was originally published in <a href="https://medium.com/revolut">Revolut Tech</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The fundamentals of iOS at Revolut]]></title>
            <link>https://medium.com/revolut/the-fundamentals-of-ios-at-revolut-f75acb765ac8?source=rss----44c5ac415e14---4</link>
            <guid isPermaLink="false">https://medium.com/p/f75acb765ac8</guid>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[revolut]]></category>
            <category><![CDATA[fundamentals]]></category>
            <category><![CDATA[development-process]]></category>
            <dc:creator><![CDATA[Illia Kucheriavyi]]></dc:creator>
            <pubDate>Tue, 14 May 2024 11:41:25 GMT</pubDate>
            <atom:updated>2024-05-22T10:53:25.705Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*JDf6G1utNopijBG5" /></figure><p>Let’s take a look behind the scenes of the iOS development at Revolut, where we strive to provide exceptional financial services and seamless customer experiences for the customers who rely on our iOS apps to manage their financial lives.</p><p>We believe that it’s possible to deliver high-quality software, fast. To achieve this we follow a set of important principles and apply a number of modern agile practices, mainly inspired by XP (eXtreme Programming).</p><h3><strong>Deliver fast</strong></h3><p>Currently, on the iOS platform, we have both applications and SDKs.<br>Here’s a list of what’s live:</p><h4>Revolut, Revolut &lt;18, Revolut Business, Revolut POS and Revolut Pay SDK.</h4><p>All iOS applications are updated in the App Store on a weekly basis. We operate on the principles of continuous integration and continuous delivery to swiftly deliver new functionality to customers — ‌as soon as a feature or part of it is ready.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-m5A-SF-uzLghCeetMMUtA.png" /></figure><p>We’ve built our own in-house framework for feature-based releases. According to our process, the Product Owner specifies a feature to be enabled starting from a selected version of the application. Then both the application version and its features are rolled out gradually, starting with a small percentage of our customer base. These two approaches — feature flagging and gradual version and feature rollout — enable delivering value to customers with the minimum amount of risk. If anything goes wrong, the flag will be turned off or the older version will be re-submitted as rollback, to remove the problematic change. Moreover, feature flags allow team members to easily switch between different user experiences, compare them, or turn them on and off for testing.</p><p>We’ve also developed an in-house experimentation platform that allows teams to experiment with new features and measure their impact using predefined metrics in a statistically significant way. This platform guarantees that experiments are properly set up, and that results are produced using a robust statistical framework.</p><p>All ideas are tested, with decisions being made using data-driven logic, making sure that only the best user experiences receive major development efforts. This results in a rapid and focused development of key user-centric features.</p><h3>Architecture</h3><p>As we delve into iOS fundamentals, it’s crucial to recognise that our iOS applications are just one piece of a larger ecosystem. Our aim is to cultivate cutting-edge technology to enhance user experiences‌ — ‌keeping it both straightforward and effective. To ensure alignment across our diverse range of apps, features, and teams, we rely on a structured process known as System Design Review (SDR).</p><p>The SDR process serves as a crucial mechanism for evaluating and refining the architecture of our solutions. Architecture acts as the glue, seamlessly merging technological elements to create a unified and effective system. In the context of SDR, mobile architecture is especially significant, guaranteeing that our iOS solutions are sturdy, scalable, in line and above industry standards.</p><p>Understanding solution-level architecture not only facilitates effective participation in the SDR process but also presents an opportunity for personal and professional growth within Revolut.</p><p>From the iOS platform perspective, all our products are implemented with Clean Architecture and MVVM-C.</p><p>Our apps aren’t monolithic. They include shared modules such as the core module, UI components‌, and feature modules like trading, payments, crypto, loans, and cards.</p><p>Currently we have:</p><ul><li><strong>160</strong> shared modules</li><li><strong>170</strong> modules in Revolut</li><li><strong>80</strong> modules in Revolut Business</li><li><strong>45</strong> modules in Revolut &lt;18</li></ul><p>All our apps and shared modules are stored in a single mono-repository, and each module can be built and run independently.</p><p>Brief highlights of modular architecture:</p><ul><li>Faster build times — every module has an example or demo project to develop it in isolation quickly without compiling the whole app</li><li>Faster development resulting from faster compiling and easy access to screens in the example project</li><li>Isolated environment for trying new things</li><li>Tests running in seconds</li></ul><h3>Tech stack</h3><p>We develop using Swift and use iOS SDKs. Our goal is to reduce dependency on third-party frameworks to minimise reliance on external code. For example, instead of relying heavily on external frameworks, we’ve adopted a lightweight reactive approach within our core library. This strategy mirrors the functionality of Combine and will facilitate a smoother transition to SwiftUI in the future with minimal effort.</p><p>With our app we have heavily exceeded the limits of XCode build capabilities and to ensure we’re corresponding with our main principle <em>of delivering quality software fast</em> we use a complex of fine-tuned open source and in-house tools.</p><p>To be able to build and test our applications and frameworks rapidly, we switched to Bazel by Google which provides reliable cloud and local modules build cache.</p><p>Our in-house tools are fully built in Swift (to be understood and modified by every iOS engineer in the company) as a command line interface and give the ability to easily operate with the whole code base.</p><p>We can build, test, and deploy easily as our CI config is similar to the local one. Which means that builds could be created and tested anywhere: from any environment as remote or local, and from every branch as release, develop, specific feature, and can be published to TestFlight and AppCenter.</p><p>For a better understanding of the development scale, let’s look at the numbers. Each month, our iOS engineers create around <strong>2,200</strong> Pull Requests which result in nearly <strong>4,000–5,000</strong> CI builds.</p><p>Having such intensive development, we also optimise our configs and module structure relentlessly. For example, the latest such optimisation helped to decrease PR validation time from <strong>40 to 10</strong> minutes, which resulted in nearly <strong>2,000–2,500</strong> CI hours decrease per month.</p><p>Also worth noting are other tools and technologies that we use, including:</p><ul><li>Git</li><li>Xcode</li><li>Figma</li><li>CoreData for data storage (all our applications provide offline viewing, so data from CoreData cache database can be shown on the screen when no internet connection is reachable and the customer needs to look up card details, for example)</li><li>SwiftLint, TeamCity, and others</li></ul><h3>Cross-functional teams</h3><p>We have more than <strong>120</strong> highly skilled iOS developers spread among different teams and locations. There’s established code ownership where each team is responsible for maintaining one or more modules. Each team typically consists of developers for iOS, Android, Web, and Backend, a Product Owner, a Designer, and a Data Analyst. So each team is like a small start-up that can come up with ideas and turn them into products or features.</p><p>Team members have full control over the functionality they develop. For an engineer, being part of a cross-functional team means having the opportunity to influence the product, participate in decision-making, and engage in learning technologies outside their expertise.</p><p>To increase our team’s productivity, we use a lot of tools for disruption-less development with respect to distributed ways of working. Surely, every team is unique in their own setup and process, but in general, we follow Agile methodologies, communicate in Slack, store our development artifacts in Confluence, and track our progress in Jira.</p><h3>Quality</h3><p>To ensure the rapid delivery of high-quality software, we prioritise maximising the automation of our quality assurance process. Our testing pyramid forms a solid foundation, with every test utilised for our iOS products being written in Swift and leveraging native SDKs such as XCTest and XCUITest. Additionally, for syntax sugar and documentation-driven test development, we incorporate frameworks like Quick + Nimble, featuring contexts and behaviour-driven testing. Furthermore, for reusable design components, we implement SnapshotTestCases.</p><p>Here are some numbers regarding our automated part of the testing pyramid:</p><ul><li>On the lowest level we have around <strong>12,000</strong> unit test suites with more than <strong>150,000</strong> separate test cases to verify individual components.</li><li>On the middle and top level, we have <strong>3000 </strong>UI tests cases covering both integration and the UI/UX part of the application</li></ul><p>Also it’s important to mention that ensuring the quality of our products is a shared responsibility among all our employees. While we don’t have dedicated QA engineers, our developers, product owners, designers, and all other employees take comprehensive measures to maintain high standards. They incorporate various testing methods into their workflow, including manual cross-checks and everyday app usage testing. By adopting this automated and collaborative approach to quality assurance, we uphold our commitment to delivering reliable and user-friendly products to our customers.</p><h3><strong>Security</strong></h3><p>Our dedicated Information Security (InfoSec) team plays a pivotal role in safeguarding Revolut’s systems and user data. Through rigorous code reviews, penetration testing, automated vulnerability scanning and proactive threat hunting. Their unwavering commitment ensures that our users can trust Revolut with their sensitive information, knowing that every measure has been taken to protect their privacy and security.</p><p>In particular, for our iOS platform, we implement complex security measures to uphold the integrity of our application and safeguard user data. We employ encryption techniques and leverage the Keychain to securely store sensitive data and credentials, ensuring they remain inaccessible to unauthorised entities, even in the event of device compromise. Furthermore, we meticulously design our API endpoints and local storage mechanisms to minimise data transfer and stored data.</p><p>To defend against potential threats from unsafe devices and runtime modifications, we integrate advanced runtime protections and detection mechanisms into our app’s codebase. These measures actively monitor the app’s execution environment, detecting and responding to any unauthorised attempts to tamper with or exploit the application. By proactively addressing such risks, we reinforce the resilience of our iOS platform, bolstering user trust and ensuring the confidentiality and integrity of their data.</p><h3><strong>Design consistency</strong></h3><p>All UI components are designed for high reusability within our applications and are housed within separate modules to uphold consistency in the app’s visuals. These modules are implemented natively across all platforms we develop for, ensuring the same UI and UX implementation. We call this approach a Design System. We’ve developed this system with the core principles of atomic design in mind.</p><p>The Design System serves as a resource-saving tool, reducing both code volume and development time. Developers no longer need to duplicate code or spend time reinventing components. Instead, they can utilise the Revolut Design System App, which showcases all existing UI components, allowing developers to easily select and reuse the appropriate components for their features.</p><h3><strong>Join Revolut</strong></h3><p>Want to build a world-class financial app for iOS? <br>Explore the <a href="https://www.revolut.com/careers">career opportunities at Revolut</a>, and join the team of the most talented developers.</p><h3><strong>Links</strong></h3><p><strong>Our Applications:</strong><br><a href="https://apps.apple.com/us/app/revolut-send-spend-and-save/id932493382">Revolut<br></a><a href="https://apps.apple.com/us/app/revolut-18/id1499857038">Revolut &lt;18<br></a><a href="https://apps.apple.com/gb/app/revolut-business/id1436969262">Revolut Business<br></a><a href="https://apps.apple.com/us/app/revolut-pos/id1604861598">Revolut POS<br></a><a href="https://developer.revolut.com/docs/guides/accept-payments/payment-methods/accept-payments-via-revolut-pay2/accept-payments-via-revolut-pay2-mobile/">Revolut Pay SDK</a></p><p><strong>Approaches:</strong><br>​​<a href="https://martinfowler.com/articles/practical-test-pyramid.html">https://martinfowler.com/articles/practical-test-pyramid.html<br></a><a href="https://bradfrost.com/blog/post/atomic-web-design/%5C">https://bradfrost.com/blog/post/atomic-web-design/</a></p><p><strong>Tools:</strong><br><a href="https://bazel.build/">https://bazel.build/<br></a><a href="https://github.com/realm/SwiftLint">https://github.com/realm/SwiftLint<br></a><a href="https://github.com/Quick/Quick">https://github.com/Quick/Quick<br></a><a href="https://github.com/uber/ios-snapshot-test-case">https://github.com/uber/ios-snapshot-test-case</a></p><p><strong>More about Revolut:</strong><br><a href="https://medium.com/revolut/under-the-hood-engineering-at-revolut-2dc183c04228">https://medium.com/revolut/under-the-hood-engineering-at-revolut-2dc183c04228<br></a><a href="https://www.revolut.com/careers">https://www.revolut.com/careers<br></a><a href="https://www.youtube.com/watch?v=SPDnhMg7kMk&amp;t=1898s">https://www.youtube.com/watch?v=SPDnhMg7kMk&amp;t=1898s</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f75acb765ac8" width="1" height="1" alt=""><hr><p><a href="https://medium.com/revolut/the-fundamentals-of-ios-at-revolut-f75acb765ac8">The fundamentals of iOS at Revolut</a> was originally published in <a href="https://medium.com/revolut">Revolut Tech</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Canvas-based JavaScript Blur detection]]></title>
            <link>https://medium.com/revolut/canvas-based-javascript-blur-detection-b92ab1075acf?source=rss----44c5ac415e14---4</link>
            <guid isPermaLink="false">https://medium.com/p/b92ab1075acf</guid>
            <category><![CDATA[canvas]]></category>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[blur-detection]]></category>
            <dc:creator><![CDATA[Eremingeorgy]]></dc:creator>
            <pubDate>Mon, 22 Jan 2024 08:38:54 GMT</pubDate>
            <atom:updated>2024-01-22T08:38:54.051Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*DeQuMVRApTGbi16I0-UqLA.png" /></figure><h3>Introduction</h3><p>While developing the Know Your Customer (KYC) module in the banking sector, we faced a specific challenge. KYC is a process where banks verify the identity and assess the risk profile of their clients. As part of this module’s development, we needed a JavaScript library that was capable of analysing images — specifically, we required it to detect if an image is blurred and, importantly, to determine the degree of that blur. This functionality was crucial because clear images are essential for accurate identity verification in KYC processes so a dedicated library was needed. Unfortunately, there were no suitable solutions in opensource that fulfilled criteria satisfactorily.</p><p>Left with no alternative, we embarked on creating an algorithm on our own. The outcome was a user-friendly algorithm that effortlessly checks any image for the presence and degree of blur. From the voluminous inquiries we observed on platforms like StackOverflow, it became evident that our solution could greatly benefit the community.</p><p>This utility has significant potential for diverse applications, such as image quality analysis on the client side. Yet, surprisingly, a modern, uncomplicated, resource-friendly library to tackle this common problem remains absent in today’s software landscape. Our experience, therefore, not only serves as a case study, but also underscores the urgent need for such a library in the JavaScript/TypeScript ecosystem.</p><h3>Relevance</h3><p>In an era where the visual web dominates, the quality of images plays a significant role in user engagement and experience. Blurry images can tarnish user perceptions and limit the effectiveness of visual communication. Yet, our research has shown a glaring gap in the JavaScript/TypeScript ecosystem: the lack of a simple, accessible tool to detect and quantify image blur. This absence impacts developers, businesses, and users alike, leaving a critical need unmet.</p><p>In the digital age, the quality of visuals is paramount across a broad spectrum of platforms. From e-commerce sites, where product images are pivotal in influencing purchasing decisions, to online galleries showcasing artists’ portfolios, news portals highlighting photojournalism, and even social media platforms where the clarity of a shared image can dictate its virality. Consider for example a retailer on Aliexpress. Take a look at the images below. It is impossible to determine how the product looks and what is written in the explanation to it because images are blurry.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/726/0*pZmFV98YGdewk129" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/726/0*BCXDjMf9gZOlbSGq" /></figure><p>Even a minor degradation in an image can turn potential buyers away. Although there are some guidelines for file formats and sizes, maintaining the intrinsic quality of an image, ensuring aspects such as clarity, remains a daunting task. Among various determinants of image quality, blur stands out as a predominant issue, impacting both amateur snapshots and occasionally even professionally curated images.</p><p>Addressing this glaring need, the proposed open-source blur detection tool for the JavaScript/TypeScript ecosystem goes beyond just identifying blurs. It aims to provide real-time feedback to users like shop owners, photographers, or journalists, prompting immediate rectification. Such a tool not only ensures that images meet the highest visual standards but also enhances user trust and engagement, reinforcing the idea that in the modern digital landscape, the clarity of an image is often synonymous with its value. The goal of the proposed blur detector is to introduce automated quality checks in the browser. This could provide feedback to users and potentially prompt them to upload images of higher quality.</p><h3>Current solutions</h3><p>This part spotlights other solutions and a crucial need in the software landscape: an accessible and efficient blur-detection library in JavaScript/TypeScript.</p><h4>1. OpenCV approach</h4><p>Solutions such as <a href="https://github.com/rahullahoria/js-blur-image-detection">js-blur-image-detection</a>, based on the heavy OpenCV library makes it an impractical choice for many applications for the multiple reasons:</p><p><strong>Performance Overhead with OpenCV</strong>:</p><ul><li><strong>Size</strong>: OpenCV is a comprehensive computer vision library. While this means it’s feature-rich, it also means it’s heavy. For web applications, using such a bulky library can significantly increase the size of your bundled JavaScript, leading to longer load times.</li><li><strong>Complexity</strong>: OpenCV offers a myriad of functionalities, many of which may not be needed for a simple blur detection task. This can add unnecessary complexity to the application.</li></ul><p><strong>Deployment Challenges</strong>:</p><ul><li><strong>Browser Compatibility</strong>: OpenCV, given its origins and extensive functionality, might not be entirely optimised for all browsers. This can lead to potential compatibility issues.</li><li><strong>Version Maintenance</strong>: With a complex library like OpenCV, staying updated with the latest versions or dealing with potential security vulnerabilities can become a challenge.</li></ul><p><strong>Runtime Efficiency</strong>:</p><ul><li><strong>Processing Time</strong>: Due to its comprehensive nature, operations in OpenCV might be more resource-intensive, potentially leading to slower runtimes, especially on less powerful devices (eg. mobile devices) or in browsers with multiple tabs and operations.</li></ul><p><strong>Dependency Concerns</strong>:</p><ul><li><strong>External Dependencies</strong>: Relying on large external libraries can sometimes introduce bugs or conflicts with other packages, making debugging more challenging.</li></ul><p>On the other hand, a zero-dependency JS/TS solution offers:</p><ol><li><strong>Lightweight &amp; Fast</strong>: With a dedicated focus on blur detection, a zero-dependency solution would be optimised for that sole task, ensuring swift performance without unnecessary overhead.</li><li><strong>Simplified Maintenance</strong>: A specialised JS/TS library would likely have fewer updates, and when updates do occur, they would be directly relevant to the blur detection task.</li><li><strong>Better Integration &amp; Compatibility</strong>: Being designed specifically for the JS/TS ecosystem ensures better compatibility with other JS libraries and frameworks. It’s also more likely to be optimised for a wide range of browsers.</li><li><strong>Enhanced Security</strong>: Fewer dependencies mean a reduced surface area for potential security vulnerabilities.</li></ol><p>In summary, while OpenCV-based solutions like the one mentioned provide a vast array of features, they might be akin to using a sledgehammer to crack a nut when it comes to specific tasks like blur detection in web applications. A zero-dependency JS/TS solution, tailored for this particular problem, offers a more efficient, maintainable, and user-friendly approach.</p><h4>2. Plain JS approach</h4><p>The best solution found so far is <a href="https://github.com/timotgl/inspector-bokeh">inspector-bokeh: Canvas-based blur detection with JavaScript</a>. The approach here is based on this<a href="https://stefan.winklerbros.net/Publications/icip2002.pdf"> article</a>. The provided algorithm showcases a detailed method for blur detection in images. It begins with a preprocessing step to blur the image slightly with the intention to enhance edge detection. This is followed by the utilisation of a Sobel kernel convolution to detect edges in the luminance-converted image. The core of the blur detection leverages these edges to calculate the average edge width, which is then used to infer the level of blur in the image. Some points of consideration:</p><ol><li><strong>Typescript Compatibility</strong>: This algorithm, while detailed, does not cater to TypeScript-specific applications. Absence of type annotations and interfaces makes its direct adoption into TypeScript projects challenging.</li><li><strong>Usability &amp; Dependency</strong>: The algorithm has a dependency on <em>canvasfilters</em>, which is sourced from an arbitrary GitHub repository. This presents two primary concerns: firstly, the potential for usability issues if the library is not well-maintained or lacks comprehensive documentation. Secondly, there’s a security risk associated with depending on unverified repositories.</li><li><strong>Size-Dependent Metric</strong>: The algorithm’s blur metric, which takes the average edge width as a proportion of the image’s width (avgEdgeWidth / width * 100), may yield inconsistent results for images of varying dimensions. This relationship could result in misleading blur assessments for either very large or small images.</li><li><strong>Complexity</strong>: The algorithm, though comprehensive, presents a steep learning curve with its multiple stages of processing and a myriad of parameters. Its intricacy could pose challenges for developers seeking a plug-and-play solution.</li></ol><p>In conclusion, this algorithm offers a methodical approach to blur detection. However, issues related to TypeScript compatibility, dependency clarity, and a questionable size-driven metric, compounded by its inherent complexity, could limit its applicability and accuracy in diverse scenarios.</p><h3>Proposed solution</h3><p>The solution is a lean and focused library designed to tackle one specific problem: analysing an image and returning a quantified blur score. This library takes a minimalist approach, intentionally avoiding dependencies, style exports, or unnecessary frameworks. What it does export is a simple TypeScript function, embodying the essence of “do one thing and do it well”.</p><p>Within this package, all the necessary mathematical computations are internalised, thus keeping the interface clean and straightforward. Most importantly, the underlying blur detection algorithm has been carefully crafted and thoroughly vetted by our data science team. This ensures that not only is it effective and reliable, but it’s also optimised to deliver accurate results swiftly. This library, therefore, represents a streamlined, specialised solution to the image blur detection problem, designed to integrate seamlessly into modern development workflows.</p><h3>Approach description</h3><p>Like many blur detection algorithms, this solution is fundamentally based on an edge detection algorithm. The process begins by passing the input image through a sequence of filters. Initially, the image is blurred to reduce high-frequency noise. Subsequently, it is transformed into grayscale to simplify the image structure, which is then convolved using the Laplacian aperture, further enhancing the image’s features.</p><p>To expedite the blur detection process, an image reduction technique is employed to decrease the number of pixels in the image without compromising on the critical details. Finally, the principal algorithm is executed, which locates the edges within the processed image. The density and clarity of these edges form the basis of the blur score returned by our function.</p><p>This series of operations forms a robust and efficient pipeline for blur detection. Our library, therefore, stands as a streamlined and effective tool for image analysis, offering an accurate blur score as a measure of image clarity.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/528/0*qCIYo4lZnWvKkO8P" /></figure><h3>Helper functions</h3><p>While developing this blur-detection solution, I chose not to use external libraries for image filtering due to the lack of quality TypeScript libraries. Instead, it was decided to craft custom helper functions, to reduce library’s size and complexity. This choice results in a streamlined, performance-optimised library that could be easily integrated into other projects.</p><h4>1. Grayscalling</h4><p>Images often use the RGBA format: Red, Green, Blue, and Alpha (for transparency). If an image is grayscaled then Red, Green, and Blue values for any pixel will be the same. Storing all three identical values is redundant. The code provided optimises this by keeping only one channel — in essence, transforming the image into a grayscale representation. The implementation of the algorithm is shown below.</p><pre>export const greyScale = (pixels: ImageData): ImageData =&gt; {<br>  const output = createImageData(pixels.width, pixels.height)<br>  const dst = output.data<br>  const d = pixels.data<br>  for (let i = 0; i &lt; d.length; i += 4) {<br>    const r = d[i]<br>    const g = d[i + 1]<br>    const b = d[i + 2]<br>    // CIE luminance for the RGB<br>    const v = 0.299 * r + 0.587 * g + 0.114 * b<br>    dst[i] = v<br>    dst[i + 1] = v<br>    dst[i + 2] = v<br>  }<br>  return output<br>}</pre><h4>2. Reducing Image data for efficiency</h4><p>When working with image data, especially in the context of web applications, it’s often crucial to reduce memory usage and improve processing speed. One common approach is to reduce image data. In the given code snippet, it is achieved by transforming an image with RGBA channels into a single-channel grayscale representation.</p><pre>const reducedPixels = (imageData: ImageData): Uint8ClampedArray[] =&gt; {<br>  const { data: pixels, width } = imageData<br>  const rowLen = width * 4<br>  let i<br>  let x<br>  let y<br>  let row<br>  const rows = []<br><br>  for (y = 0; y &lt; pixels.length; y += rowLen) {<br>    row = new Uint8ClampedArray(imageData.width)<br>    x = 0<br>    for (i = y; i &lt; y + rowLen; i += 4) {<br>      row[x] = pixels[i]<br>      x += 1<br>    }<br>    rows.push(row)<br>  }<br>  return rows<br>}</pre><h4>3. Initial edge smoothing</h4><p>A Gaussian blur is then applied to smooth out any high-frequency noise that could disrupt edge detection. The implementation of the algorithm is shown below.</p><pre>export const gaussianBlur = (pixels: ImageData, diameter: number): ImageData =&gt; {<br>  diameter = Math.abs(diameter)<br>  if (diameter &lt;= 1) return pixels<br>  const radius = diameter / 2<br>  const len = Math.ceil(diameter) + (1 - (Math.ceil(diameter) % 2))<br>  const weights = getFloat32Array(len)<br>  const rho = (radius + 0.5) / 3<br>  const gaussianFactor = 1 / Math.sqrt(2 * Math.PI * rho * rho)<br>  const rhoFactor = -1 / (2 * rho * rho)<br>  let wsum = 0<br>  const middle = Math.floor(len / 2)<br>  for (let i = 0; i &lt; len; i++) {<br>    const x = i - middle<br>    const gx = gaussianFactor * Math.exp(x * x * rhoFactor)<br>    weights[i] = gx<br>    wsum += gx<br>  }<br>  for (let i = 0; i &lt; weights.length; i++) {<br>    weights[i] /= wsum<br>  }<br>  return separableConvolve(pixels, weights, weights)<br>}</pre><h4>4. Edge enhancement</h4><p>The final step uses a Laplacian aperture to enhance the image’s edges, readying it for the core edge-detection process in our blur algorithm. These functions help the tool remain compact, reliable, and independent, providing accurate blur scores without the need for external dependencies.</p><h3>Core algorithm</h3><p>Blur check is done by calculating a variance matrix of<a href="https://docs.opencv.org/3.4/d5/db5/tutorial_laplace_operator.html"> Laplacian</a> operator applied to the input. The implementation follows<a href="https://pyimagesearch.com/2015/09/07/blur-detection-with-opencv/"> that</a> post.</p><p>Steps:</p><ul><li>Remove noise by applying a Gaussian blur and then convert the original image to grayscale</li><li>Apply a Laplacian operator to the grayscale image</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/182/0*30iaTeuS7DYJrdHh" /><figcaption>maths formula of Laplace function</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/86/0*j1ztR9BtEssmbhl1" /><figcaption>Laplacian kernel to apply to an image</figcaption></figure><p>That produces a matrix of second derivatives and variance of second derivatives will be a blurriness score. If we want to find blurry images we should adjust the threshold, e.g. 5. Of course threshold adjustment should be done on real data.</p><p>The basic edge analysis algorithm is given below.</p><pre>const detectBlur = (pixels: Uint8ClampedArray[]) =&gt; {<br>  const width = pixels[0].length<br>  const height = pixels.length<br>  const getMean = () =&gt; {<br>    let sum = 0<br>    for (let i = 0; i &lt; height; i++) {<br>      for (let j = 0; j &lt; width; j++) {<br>        sum += pixels[i][j]<br>      }<br>    }<br>    return sum / (width * height)<br>  }<br>  const getVariance = (mean: number) =&gt; {<br>    let sum = 0<br>    for (let i = 0; i &lt; height; i++) {<br>      for (let j = 0; j &lt; width; j++) {<br>        const normalized = pixels[i][j] - mean<br>        sum += normalized * normalized<br>      }<br>    }<br>    return sum / (width * height)<br>  }<br>  return getVariance(getMean())<br>}</pre><p>In order to determine the blur score using the approach described above, the code below could be used.</p><pre>const BLUR_BEFORE_EDGE_DETECTION_DIAMETER = 3.0 // pixels<br>const LAPLACIAN_APERTURE = [0, 1, 0, 1, -4, 1, 0, 1, 0]<br><br>const prepareImage = (imageData: ImageData) =&gt; {<br>  const preBlurredImageData = gaussianBlur(imageData, BLUR_BEFORE_EDGE_DETECTION_DIAMETER)<br>  const greyScaled = greyScale(preBlurredImageData)<br>  return convolve(greyScaled, LAPLACIAN_APERTURE)<br>}<br>export const measureBlur = (imageData: ImageData): number =&gt;<br>  detectBlur(reducedPixels(prepareImage(imageData)))</pre><h3>Conclusions</h3><p>Proposed approach is more streamlined then existing ones because it has no dependencies and focuses on variance as a measure of blur. It consists of fewer operations and has a more straightforward process which might lead to better performance, especially when applied iteratively or on numerous images.</p><p><strong>Advantages</strong>:</p><ul><li>Simplicity makes it easy to understand, modify, and maintain.</li><li>The Laplacian-based approach is a classic method for blur detection, relying on the principle that a sharp image will have a high variance of intensities in its edges.</li><li>It does not depend on external libraries like <em>canvasfilters</em> or openCV.</li></ul><p><strong>Disadvantages</strong>:</p><ul><li>May not be as nuanced or detailed in its evaluation as existing solutions.</li><li>Could potentially miss subtle details or nuances in certain images.</li></ul><p>In summary, the proposed algorithm’s simplicity could suffice for most general blur detection tasks. It makes it easier to integrate, adapt, or extend. The existing solutions, due to its dependence on other libraries, might have some constraints. If speed, simplicity, and independence from external libraries are paramount, then this solution would be the way to go.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b92ab1075acf" width="1" height="1" alt=""><hr><p><a href="https://medium.com/revolut/canvas-based-javascript-blur-detection-b92ab1075acf">Canvas-based JavaScript Blur detection</a> was originally published in <a href="https://medium.com/revolut">Revolut Tech</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Evolution of translations management for Android at Revolut]]></title>
            <link>https://medium.com/revolut/evolution-of-translations-management-for-android-at-revolut-2f97b5969bfb?source=rss----44c5ac415e14---4</link>
            <guid isPermaLink="false">https://medium.com/p/2f97b5969bfb</guid>
            <category><![CDATA[kotlin]]></category>
            <category><![CDATA[gradle]]></category>
            <category><![CDATA[translation]]></category>
            <category><![CDATA[gradle-plugin]]></category>
            <category><![CDATA[android]]></category>
            <dc:creator><![CDATA[Yaroslav T]]></dc:creator>
            <pubDate>Tue, 05 Dec 2023 17:28:51 GMT</pubDate>
            <atom:updated>2023-12-05T17:28:50.917Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xVflOy5xgdNVBDbIRR9qHg.png" /></figure><p>Here’s everything you need to know about how translations have evolved at <strong>Revolut</strong>. We currently have 6 apps, with 330 modules containing <em>strings.xml</em> files. And don’t forget, we need to support 33 languages.</p><p>To make the translation process smooth, we use the API of <a href="https://lokalise.com/">Lokalise</a>.</p><p>When one of our engineers adds a new string on the platform, it’s translated into all languages within 1 hour. After that, we need to download and place the translations into the correct module. Now, from an engineering perspective, here comes the most interesting part!</p><h4>Initial implementation</h4><p>In 2017, we began our expansion, and the first supported languages were French and Polish. We quickly reached a stage where fast translation became an integral part of the feature release process.</p><p>Using our partner, Lokalise.com, we started implementing translations for a single app (only Revolut retail app existed at the time).</p><p>We added Lokalise <a href="https://developers.lokalise.com/docs/android-sdk-v1">library</a> to our project and used keys from it via a Singleton pattern:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/b55574683f8ebc328b4049d46f1114bf/href">https://medium.com/media/b55574683f8ebc328b4049d46f1114bf/href</a></iframe><p>Library downloads translations from the server once the user opens the app. This approach was fast to implement, but had 2 limitations:</p><ol><li>API has user limit, which is critical for the fast-growing application</li><li>Download translations on app launch degrades the performance</li></ol><h4>First script</h4><p>To answer both of these problems, we created a simple local script that downloads strings and puts them into the resources’ folder. Before releasing a new version of the app, one of the engineers has to call this script and merge translated files into the <em>release</em> branch.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/78c6e72e4148de66b8d461b467b0df55/href">https://medium.com/media/78c6e72e4148de66b8d461b467b0df55/href</a></iframe><h4>Simplifying developers’ life</h4><p>Later on, one of our engineers introduced an additional script to upload keys to Lokalise. The goal was to let engineers create new keys in strings.xml during the development process, automatically upload them to the provider, translate them, and then download them back.</p><p>However, we soon discovered that this approach caused confusion: what should be the source of truth, if an iOS developer added the same key on the website, but an Android dev used the script, how to pick the correct key? As a result, after just one month, we decided to disable the new script.</p><h4>Migration to Groovy</h4><p>Python script was executed using a Gradle task:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/573f3122e41a7e2ecb4db0d92012a8f5/href">https://medium.com/media/573f3122e41a7e2ecb4db0d92012a8f5/href</a></iframe><p>Why do we need to wrap it, when we can easily write the same simple script in Groovy? If we’re going to run it on CI, we can remove dependency on Python. After 3 months of using Python script, we migrated to Groovy.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/10624bd5a8ffdc1d5d805e4f854ea605/href">https://medium.com/media/10624bd5a8ffdc1d5d805e4f854ea605/href</a></iframe><p>For the next 2 years, this script was our main way of downloading strings.</p><h4>Modularisation</h4><p>In 2019, with a large codebase (1+ million lines of code), we embarked on the journey of adapting modern approaches to address the growing build time and initiated the modularisation process. Soon, we realised that the existing script wasn’t flexible enough to be used in a multimodule environment, as it could only place files within the app module</p><p>We migrated to a more complex, but flexible solution using Yaml, Bash, and Kotlin. We used Yaml config to declare all required data, such as token, project ID (each app is represented by a different Localise project), languages, module names, and their corresponding files:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/fc7b73592f3f24f4b95a064d8e1b0e45/href">https://medium.com/media/fc7b73592f3f24f4b95a064d8e1b0e45/href</a></iframe><p>After parsing this config with Kotlin code, the translations were downloaded and placed into a dedicated module. To execute the Kotlin code, we utilised a Bash script, which was unique for each application.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/54659f06c25e0964c789be1465e3cc29/href">https://medium.com/media/54659f06c25e0964c789be1465e3cc29/href</a></iframe><p>It was a working solution, but complicated and overwhelming. Let’s highlight the key disadvantages we faced:</p><ul><li>Messy Yaml configs: same modules in different configs, hundreds lines of configuration in one file</li><li>Solution fragility in case of failure: if one config was broken, it caused downloading issues for all Revolut apps</li><li>Difficulty in sharing modules between apps: making modules shared between apps wasn’t straightforward, especially when translations belonged to different projects on Lokalise. For example, sharing a module from Revolut Business with Revolut &lt;18 app required the separation of Bash scripts and Yaml configs.</li><li>Hard to modify Bash script because we need to modify and test all 6 for each app every time</li><li>Inability to execute in parallel: the existing setup didn’t allow for parallel execution, slowing down the process.</li><li>Bash scripts and configs are hard to cover with unit or integration tests, leading to lack of good test coverage</li></ul><h4>Gradle solution</h4><p>We decided to implement a custom <a href="https://docs.gradle.org/current/userguide/custom_plugins.html">Gradle</a> plugin and move the existing downloading logic there. We can pass all required parameters, such as token or file name into Gradle configuration to get rid of Yaml. To get rid of Bash, we can create a Gradle task and execute it straight away for the module.</p><p>Let’s see how it works for the <em>feature_payments</em> module we saw in Yaml config. We need to add these lines to its build.gradle file:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/305887e29438281159297fe96c9ce5a0/href">https://medium.com/media/305887e29438281159297fe96c9ce5a0/href</a></iframe><p>To download translations, we have to execute our new task <strong>downloadStrings</strong> using this simple line in the terminal:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/df0876b9cd51e48f977e9453481c0535/href">https://medium.com/media/df0876b9cd51e48f977e9453481c0535/href</a></iframe><p>To download Strings for all apps, we can run <em>./gradlew downloadStrings</em></p><h4>How the task works under the hood</h4><ol><li>Clear all files in the module with pattern strings_*.xml</li><li><a href="https://developers.lokalise.com/reference/download-files">Download</a> translations archive from Lokalise api</li><li>Unzip and place files in the module</li></ol><p><strong>Pros of the new approach:</strong></p><ul><li>Each module is configured independently. No more Yaml configs with 300 lines of configuration</li><li>Making modules shared between apps has become effortless, requiring no changes to the configuration</li><li>No need to maintain multiple Bash scripts</li><li>Downloading and moving files can be done in parallel, speeding up the process</li><li>Unit and integration tests on Kotlin included into the CI pipeline to make sure everything works as expected</li><li>The plugin is written on Kotlin, a language familiar to every Android developer in the company, making it easy for them to contribute</li></ul><h4>Improvements</h4><p>After applying configuration to 300+ modules, we noticed 3 fields in Gradle configuration are the same for each app: projectId, apiToken and languages. To encapsulate them, we used the <a href="https://docs.gradle.org/current/samples/sample_convention_plugins.html">Gradle conventional plugin</a>.</p><p>Inside a new plugin, we created a simple Enum class with all our projects:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/bc808acfa63a8df89e07fabdec2c43e5/href">https://medium.com/media/bc808acfa63a8df89e07fabdec2c43e5/href</a></iframe><p>Instead of writing 3 boilerplate fields, the developer has to specify only <strong>projectName</strong>. The rest would be applied inside the conventional plugin. This is how final configuration looks like:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/9d5a648429231028bb989eee7b9bbbf5/href">https://medium.com/media/9d5a648429231028bb989eee7b9bbbf5/href</a></iframe><p>Code from Conventional plugin:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/eb675741d4d081eacfc2514bf6b63763/href">https://medium.com/media/eb675741d4d081eacfc2514bf6b63763/href</a></iframe><h4>Other problems we solved</h4><p><strong>Duplicated files</strong></p><p>We couldn’t have the same strings files in different modules because of transitive R classes, which couldn’t contain duplicates. To fix it, we enabled <a href="https://developer.android.com/studio/build/optimize-your-build#use-non-transitive-r-classes">non-transitive R classes</a>. Now we have to specify the full path to the R in code (e.g. <em>com.revolut.feature.payments.R.string.my_title</em>), but we can make the same file be added to multiple modules.</p><p><strong>Merge conflicts</strong></p><p>Strings files can change quite often if multiple people work on the same feature. In such cases, we might encounter numerous merge conflicts and even risk breaking the main branch.</p><p>For instance, if one person merges a new string “A” and its corresponding code, and later another person merges a file without the string “A,” it can lead to complications and errors.</p><p>We fixed this issue with 2 simple rules:</p><ol><li>No one can merge strings xml files manually. We have a <a href="https://github.com/danger/kotlin">Danger</a> rule to block such Pull requests</li><li>Automated script on CI merges strings to always the same branch (to not have conflicts). Strings are merged to the <em>release</em> branch and automatically back merged to the main <em>development</em> branch. Script is automatically triggered every day or can be triggered by the engineer</li></ol><p><strong>Changed or deleted strings used by older app versions</strong></p><p>Currently, we have at least 3 app versions in the release cycle. For instance:</p><p>- version 1 is available on Google Play</p><p>- version 2 is accessible in the Beta channel</p><p>- version 3 is the one we are currently working on and releasing internally</p><p>We came across a problem: if a decision is made to remove a string that’s no longer needed in version 3, it could potentially break the release of versions 1 and 2, as they’re still utilising that string.</p><p>To prevent such issues, we have another set of rules:</p><ul><li>Don’t remove strings immediately. Move them to the separate file called <em>strings_unused.xml</em> and remove them only after some time, when we’re completely sure it won’t break anything (usually after 6 months)</li><li>If we intend to modify the number of placeholders in the string, it is essential to create a new string. We need it as an incorrect number of arguments, whether shown or not, can lead to unexpected UI and UX bugs</li><li>If an engineer wants to change any text — they’re welcome to do it</li></ul><h4>Conclusion</h4><p>Over the past 7 years, we’ve explored various solutions for translation management. While the task may not appear complicated at first glance, as you can see, there are many pitfalls.</p><p>Even though our current solution isn’t a silver bullet, it’s flexible enough to be improved upon as soon as we face a new challenge.</p><p>If you have a single app with a few modules, it probably doesn’t make sense to invest time into creating your own library; instead, you can use what’s already available or do everything manually.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2f97b5969bfb" width="1" height="1" alt=""><hr><p><a href="https://medium.com/revolut/evolution-of-translations-management-for-android-at-revolut-2f97b5969bfb">Evolution of translations management for Android at Revolut</a> was originally published in <a href="https://medium.com/revolut">Revolut Tech</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Improving product reliability by imposing constraints as a part of CI/CD process]]></title>
            <link>https://medium.com/revolut/improving-product-reliability-by-imposing-constraints-as-a-part-of-ci-cd-process-597cf2307224?source=rss----44c5ac415e14---4</link>
            <guid isPermaLink="false">https://medium.com/p/597cf2307224</guid>
            <category><![CDATA[application-security]]></category>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[devsecops]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[security]]></category>
            <dc:creator><![CDATA[Pedro Moura]]></dc:creator>
            <pubDate>Fri, 03 Nov 2023 14:26:59 GMT</pubDate>
            <atom:updated>2025-03-18T14:54:07.575Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*70stDA0NLmNffroTS_gatQ.png" /></figure><p>At Revolut, we always aim to consistently provide efficient, high-quality, and secure services. Our primary goals are to enhance product quality, increase customer satisfaction, and reduce business risk.</p><p>To achieve those goals, we should aim to be as proactive — rather than reactive — as possible. This means fixing any point of failure before it reaches production by identifying, reporting, and potentially blocking deployments that can impact the reliability of our products.</p><p>In this article, we’ll explain how we achieve those goals using automated risk calculation, data analysis, and imposing constraints to push for the mitigation of open vulnerabilities and the fix of reported bugs identified in our products.</p><h3>Challenges faced by Revolut</h3><p>Each application/service has its own specifications, with different technology stacks and architectures, bringing a diversity of security challenges and different vulnerabilities. That being said, application risk cannot be based only on the reported security vulnerabilities but also in the context of that specific application.</p><p>Continuous scanning, reporting, visibility, and risk evaluation are essential to providing the best security advice and automated security controls.</p><p>Nowadays there are several types of security scanners (SAST, SCA, DAST, IaC, etc.) that help security professionals to identify and report vulnerabilities, where each of them can belong to a different third-party provider. For further information on these, read our article on <a href="https://medium.com/revolut/security-drone-scaling-continuous-security-at-revolut-862bcd55956e">continuous security</a>.</p><p>Data scraped from different sources creates friction when it’s needed to group findings and provide mitigations. This can also decrease mean time to identify vulnerabilities and mean time to provide mitigations. Also, to avoid such negative workflow impact, <a href="https://dora.dev/devops-capabilities/technical/shifting-left-on-security/">security must be shifted left</a>.</p><p>Without a centralised source of truth for security-related application data, most of the time is wasted on searching for information/findings in segmented platforms and finding the correct people to provide the necessary details for the security assessments.</p><p>It’s also important to note that applications being deployed in production should also be controlled. By calculating the dynamic risk of applications and collecting the number of present bugs during CI/CD stages, it’d be possible to block the most vulnerable applications from being deployed. This would make sure that no critical vulnerabilities or bugs reach production, improving product reliability.</p><p>Therefore, the main problems were raised:</p><ul><li>How to push for the mitigation of existing vulnerabilities?</li><li>How to push for the fix of existing bugs?</li><li>How can data from different sources be centralised?</li><li>How can a risk score be dynamically calculated using a mathematical expression that considers not only security findings but also the context of the application?</li><li>How can that mathematical expression be easily scalable, flexible, and tuned in different situations?</li><li>How to control applications being deployed using constraints?</li><li>When emergency situations occur, how to effectively bypass a constraint decision?</li></ul><h3>Proposed Ecosystem Solution — DARC</h3><p>As explained in <a href="https://medium.com/revolut/security-drone-scaling-continuous-security-at-revolut-862bcd55956e">this article</a>, Security Drone is an all-in-one security scanner framework that can be integrated in the SDLC, developed by Application Security Team. Security Drone serves an HTTP/API that receives webhooks from our code repository to initiate a security scan against the source branch of each Pull Request (PR). Taking advantage of this existing solution, it was decided to implement constraints at PR level, shifting left as much as possible. This way Security Drone, besides scanning for security vulnerabilities, will also gain blocking functionality at each PR.</p><p>Although this solution was already in place, it wasn’t enough. We still needed to collect and store all necessary data from different data sources to provide the most accurate decisions based on risk and number of bugs. With that in mind, over the last year, we created a new application called <strong>Dynamic Application Risk Calculator</strong> (DARC).</p><p>DARC is an automated security platform that provides constraining decisions based on the correlation of internal data sources.</p><p>DARC is used to calculate the risk associated with each application, as well as evaluate how many bugs a team has open. With those computational capabilities, a blocking mode is implemented at the PR stage using Security Drone integration to scan every new and updated PR.</p><p>To support such features, DARC provides company-wide integration with several internal data sources, from which data is frequently collected.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/614/0*CvodZthzzPeacySJ" /></figure><p>DARC is divided into the following sub-components:</p><ul><li><strong>API </strong>— a backend service responsible to manage requests and integrations</li><li><strong>Collector</strong> — schedule tasks (<a href="https://github.com/agronholm/apscheduler">apscheduler</a>) used to update DARC DB with data from internal data sources, defined to refresh data at regular intervals (depending on the data source)</li><li><strong>DB</strong> — a database to store necessary data</li><li><strong>UI</strong> — a frontend to present all data</li></ul><h3><strong>Added value</strong></h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/0*EU5lFN9fDJwmHRAJ" /></figure><h3>Blocking Mode</h3><p>Two different types of blocking mode were defined in our ecosystem: one related to application risk score and another to the number of open bugs. Although both have the same main logic flow and outcomes, they should be distinct from each other to improve audit and traceability.</p><p>Risk-based blocking mode performs assessments based on the target application of the PR, which Security Drone can directly map using the repository slug. Then, the target application is sent in a request to DARC API, where the risk score will be obtained based on the security-related information associated with that target application.</p><p>Bugs-based blocking mode works by team. Security Drone maps the owner of the PR to their assigned team, then a request is sent to DARC API where the number of open bugs for that team is obtained.</p><p>For positive blocking decisions — besides blocking the PR — a message explaining why the PR was blocked is added to the target PR with a link to DARC-UI where engineers get insight into the main inputs that caused the blocking mode to trigger.</p><p>For emergency or justifiable reasons, we also implemented a derogation mechanism, that allows high-role engineers to bypass the blocking mode.</p><p>DARC also focuses on improving visibility and awareness around blocking mode, so besides adding feedback in the PR, it also sends weekly messages to global internal communication channels with the teams and applications that are currently above or near the thresholds.</p><p>Also, to improve traceability, an audit log mechanism was created that allows us to keep track of all blocked PR’s (bugs or risk-related) and their respective derogations.</p><h3>Blocking Mode Sequence Diagram</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/0*LfHBcbcltYfo1TBd" /></figure><h3>Risk-Based Blocking Mode</h3><p>The main goal of risk-based blocking mode isn’t only to reduce the number of vulnerabilities, but also to prevent vulnerabilities from reaching that production environment, consequently reducing the mitigation cost and decreasing business risk.</p><p>In DARC, the risk score is stateless, meaning that it is only calculated at the moment of each request using previously collected data related to the application context and vulnerabilities waiting to be fixed.</p><p>Risk is usually defined as likelihood multiplied by the impact. But at a lower level, it should depend on several factors, including the context of the application and the number of vulnerabilities found in each scanner and their associated severity. However, if the approach to define application risk scores requires manual work and does not have flexible metrics, it’d be hard to scale and maintain. For instance, if we want to integrate another scanner, vulnerabilities found from that scanner should be seamlessly integrated into the risk calculation. The same applies if another useful context attribute from the context of the application needs to be integrated. Therefore, a flexible and scalable mathematical expression is needed, where we can define different heights for each input.</p><h3>Risk Factors</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/0*3oaT8mYl5jXoCPyQ" /></figure><p>To calculate the risk score, DARC considers the findings from our security scanners and internal findings, evaluating the severity of each finding based on CVSS score. We ensure that all reported vulnerabilities have a CVSS score and have the target application defined, to guarantee that later we can associate each finding to its respective application. Security findings are used to calculate the Risk Factor Vulnerability (RFV) value.</p><p>Most of our scanners’ rules are created and tuned internally, with a target of less than 10% false positive rate. This is a trustworthy value that enables us to calculate RFV with precision and very low uncertainty.</p><p>Another component for the risk calculation is the Risk Factor Context (RFC). RFC will contain several child risk factors (RF) from the application context, where as a baseline we included:</p><ul><li><strong>[RF1] Data Store or Processed </strong>— Related to data sensitivity level, personal data and regulations applied. Risk should increase at the same level as the sensitivity level of data that the application handles</li><li><strong>[RF2] Exposure </strong>— Internal-accessible services with internal registered DNS aren’t exposed to the public, and therefore risk should be lower in such cases. Other types of network specifications can be used in this section. ‌For example, types of firewall rules, or if it has integration with third party services</li><li><strong>[RF3] Maintenance / Lifecycle </strong>— Security Drone scans new code. Therefore, if a service isn’t actively maintained and new code pushed, we won’t have recent scans. New vulnerabilities appear everyday and we’re constantly creating new rules under our scanners, so it’s important to scan regularly. Besides that, services that aren’t actively deployed won’t have the last version of internal updated packages (OS or software). Those are the main reasons risk should increase when an application isn’t actively maintained</li><li><strong>[RF4] Confidentiality, Integrity, and Availability (CIA) — </strong>This is the core of information security and has a higher impact on each, as higher risk must be</li><li><strong>[RF5] Security </strong>— From our security monitoring, we can extract some valuable information from this RF. For example, the number of security incidents or identified attacks on an application. This can increase the risk, as we know that malicious actors frequently target the application</li></ul><h3>Risk Mathematical Expression</h3><p>To be flexible and resilient, the DARC mathematical expression is based on a weighted arithmetic mean.</p><p>The weighted average (x) is equal to the sum of the product of each of the RF weights (wi) multiplied by the RF value (xi) and divided by the sum of the weights:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/448/0*Unj6bZK966IZ6Myr" /></figure><p>RFs can have different weights, which gives us the ability to manipulate the importance of some attributes compared with others. This makes sense depending on the amount of value that we want to give to an attribute in the final risk value, allowing us to adapt source metrics weight in a way we understand to be more meaningful. It also provides a dynamic variation, and can never be null in case some (but not all) of the RFs aren’t applicable or available.</p><p>This approach ensures scalability in terms of new source metrics that need to be added.</p><p>The approach to this problem was obtained from the following scientific investigation in the article <a href="https://downloads.hindawi.com/journals/jhe/2020/5601068.pdf">A Delphi Study to Categorize Security of Health Data and Provide Risk Assessment for Mobile Apps</a>.</p><h3>How DARC Score is Calculated</h3><h3>Baseline metrics</h3><p>The following baseline metrics were defined:</p><ul><li>Output value is defined between min=0 and max=100</li><li>Weights for each RF have values low=1, medium=2 and high=3</li></ul><p>And also the following thresholds:</p><ul><li>0–50 → Pass</li><li>50–75 → Warning</li><li>75–100 → Block</li></ul><h3>Calculating RFC</h3><p>Values available for impact of each RF in RFC are:</p><ul><li>Low=0</li><li>Isolated=30</li><li>Medium=60</li><li>High=100</li></ul><p>Weight values were defined as the most meaningful values, giving more importance to the RF values that we feel are most important. Now, let’s consider the following sample data inputs related to a random application context and their respective impact values to each RF:</p><p><strong>[RF1] Data Store or Processed (</strong>weight = <strong>3)</strong></p><ul><li>Confidential data and is regulated by PCI DSS → High=<strong>100</strong></li></ul><p><strong>[RF2] Exposure (</strong>weight = <strong>3)</strong></p><ul><li>Internal service (not exposed to public access) but integrated with one third-party provider<strong> </strong>→ Isolated=<strong>30</strong></li></ul><p><strong>[RF3] Maintenance / Lifecycle (</strong>weight = <strong>1)</strong></p><ul><li>Actively maintained and deployed every week → Low=<strong>0</strong></li></ul><p><strong>[RF4a] Confidentiality (</strong>weight = <strong>3)</strong></p><ul><li>High=<strong>100</strong></li></ul><p><strong>[RF4b] Integrity</strong> <strong>(</strong>weight = <strong>3)</strong></p><ul><li>High=<strong>100</strong></li></ul><p><strong>[RF4c] Availability</strong> <strong>(</strong>weight = <strong>3)</strong></p><ul><li>Medium=<strong>60</strong></li></ul><p><strong>[RF5] Security (</strong>weight = <strong>1)</strong></p><ul><li>No open incidents and some tentative of attack in last months → Isolated=<strong>30</strong></li></ul><p>By applying previous RF values and their respective weights, the RFC value with weighted arithmetic mean can be obtained:</p><p><strong><em>RFC </em></strong>= (3x100 + 3x30 + 1x0 + 3x100 +3x100 + 3x60 + 1x30) / (3+3+1+3+3+3+1)</p><p><strong><em>= 70.6</em></strong></p><p>Previous PoC shows that — besides being easily scalable — this isn’t a static approach. The weights and impact values of each RF can be easily adapted whenever meaningful reasons are proven, giving us the ability to evolve further down the road. It was also made sure that low-impact values for RF are rarely used to have a better balance between RFC and RFV — otherwise we could end with RFC considerably decreasing the final score.</p><h3>Calculating RFV</h3><p>RFV is calculated based in reported vulnerabilities, using the respective severity of each security finding. All of them have a CVSS score defined, and it’ll be used as a base for a risk multiplier. The risk multiplier makes sense to avoid low values and to increase the output when high or critical vulnerabilities are reported. Considering this, rules for RFV calculation are:</p><ol><li>Target application has a critical vulnerability (CVSS &gt;= 9) → RFV = 100</li><li>Target application has a high vulnerability (7 &lt;= CVSS &lt;= 8.9) → RiskMultiplier = 3</li><li>Target application has a medium vulnerability (4 &lt;= CVSS &lt;= 6.9) → RiskMultiplier = 2</li><li>If only low vulnerabilities (0.1 &lt;= CVSS &lt;= 3.9) are present → RiskMultiplier = 1</li></ol><p>Then, the final mathematical expression for RFV is:</p><p><strong>RFV</strong> =<em> sum(vulns[CVSS]) x RiskMultiplier</em></p><p>For rule number 1, RFV will automatically be a maximum of 100 since any critical vulnerability presents a maximum risk.</p><p>Now let’s consider an application that has four reported vulnerabilities:</p><ol><li>SAST finding with 8.2 CVSS score</li><li>Internal finding with 7.9 CVSS score</li><li>SCA finding with 6.5 CVSS score</li><li>DAST finding with 5.3 CVSS score</li></ol><p><strong><em>RFV </em></strong>= (8.2 + 7.9 + 6.5 + 5.3) x 3 = <strong><em>83.7</em></strong></p><h3>DARC score</h3><p>To obtain the final DARC score, RFC and RFV values must be correlated. The same logic of weighted arithmetic mean is used, allowing us to change the importance of each by manipulating the weights or even adding another risk factor. This allows us to be adaptable and easily scalable. At the moment, both RFC and RFV have the same weight (3) since we felt they have the same importance and will bring a better balance.</p><p>Considering the previous values of <strong><em>RFC=70.6 </em></strong>and<strong><em> RFV=83.7</em></strong>:</p><p><strong>DARC score</strong> = (3x70.6 + 3x83.7) / (3 + 3) = <strong>77.15</strong></p><p>The obtained DARC score is above the defined threshold (75), therefore all the PRs for the application in this example would be blocked.</p><h3>Bugs-Based Blocking Mode</h3><p>The main goal of bugs-based blocking mode is to reduce the number of bugs by prioritising bug resolution over future development, increasing product quality and customer satisfaction.</p><p>The flow for this blocking mode is similar to the logic behind the risk blocking mode, but it’s more simple to obtain the final decision.</p><p>DARC Collector frequently gets all reported bugs and stores them in DARC-DB, associating each bug to the respective team. Then, when requested by Security Drone to validate bugs blocking mode, DARC computes how many bugs each team has in some states — triaged and not closed.</p><p>If the team of the PR owner has more bugs than the allowed threshold, the PR will be blocked until bugs are fixed and the threshold is respected.</p><h3>Relaunch Blocking Validation</h3><p>Each approver action in our code repository will trigger a blocking validation flow by sending a webhook to Security Drone. This action is used to relaunch blocking validation and create derogations.</p><p>If the owner of a PR wants to relaunch the blocking validation flow after decreasing the risk or fixing bugs (to remove the blocking mode if thresholds are now respected), the owner can just approve their own PR. This will validate a new risk score or number of bugs against the thresholds. At this stage, the PR still can’t be merged since all of the PRs need to be reviewed and approved by another engineer as peer review logic.</p><h3>Derogation Mechanism</h3><p>In exceptional or urgent situations where the blocking mode needs to be bypassed, a derogation mechanism was created to overwrite the blockage.</p><p>This mechanism is only used in exceptional or urgent situations and not as a utility to skip fixing vulnerabilities or bugs. The main requirements for this mechanism are:</p><ul><li><strong>Authorisation</strong>: only engineers in the allowed approvers list will be able to create a derogation and unblock the PR. We keep this list short and restricted to AppSec, Heads of Engineering, and Directors</li><li><strong>How to create a derogation</strong>: an authorised engineer just needs to approve the blocked PR. A webhook will be sent to Security Drone that’ll validate with DARC-API if the approver is in the allowed list. If yes, the PR will be unblocked</li><li><strong>Tracking</strong>: DARC-DB will store each derogation occurrence for audit purposes</li><li><strong>Alerting</strong>: each time a derogation occurs, we receive a notification in a dedicated channel</li></ul><h3>Derogation Sequence Diagram</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/0*VDe1Q7mntIa9ot9l" /></figure><h3>What have we achieved?</h3><p>To allow teams and engineers to comply with the thresholds and create awareness around DARC, we deployed the first version in warning mode, meaning that teams and applications above the thresholds only received warning messages in the PRs. This warning period lasted for 2 weeks, and a positive effect was felt — the number of non-compliant applications and teams decreased.</p><p>After the activation of the blocking mode, we achieved:</p><ol><li>An automated and flexible way to quantify risk for each application</li><li>An effective way of increasing the quality of our products, with constraints implemented within a shift-left approach (at the PR phase). It takes approximately 5 seconds ‌to block a PR after its creation or update</li><li>More visibility and control over the global risk and quality of our products</li><li>An ecosystem to quickly identify and report (PR and weekly notifications) applications and teams that aren’t compliant with our thresholds for risk and bugs, respectively, increasing global awareness</li><li>UI that provides us with means to quickly access and filter all necessary security data for our daily work</li><li>A forecast feature that allows us to predict applications and teams that are currently not compliant with the threshold</li><li>Audit logs for every relevant action that’s taken under the ecosystem</li><li>All PRs are evaluated and triaged automatically. Since the activation of the blocking mode (~2 months), DARC evaluated 165,670 PR and blocked 1,832</li><li>Currently only 1% of our applications are above the defined risk threshold — the percentage was higher when blocking mode was announced</li><li>Decrease in the number of open bugs by ~ 31%</li><li>Clear metrics that the teams’ proactivity related to risk and bugs increased</li></ol><h3>Next Steps</h3><p>DARC will always be under development as it can be widely integrated and its use cases are enormous. On our roadmap, we have various points, some of which include:</p><ul><li>Decrease thresholds to achieve even better results and therefore products</li><li>Add more risk factors to DARC risk calculation, from internal sources</li><li>Improve DARC user interface</li><li>Use DARC capabilities to implement regular scans and on-demand scans</li><li>Integrate with more custom security scanners</li><li>Add other types of blocking modes</li></ul><h3>Credits</h3><p>Credits go to every Revolut AppSec engineer involved in the design and development of DARC and Security Drone:</p><p>Pedro Moura, Arsalan Ghazi, Krzysztof Pranczk, Alejandro Ulises, Roger Norton, Kevin Borras</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=597cf2307224" width="1" height="1" alt=""><hr><p><a href="https://medium.com/revolut/improving-product-reliability-by-imposing-constraints-as-a-part-of-ci-cd-process-597cf2307224">Improving product reliability by imposing constraints as a part of CI/CD process</a> was originally published in <a href="https://medium.com/revolut">Revolut Tech</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Customising Android app UI with themes]]></title>
            <link>https://medium.com/revolut/customising-android-app-ui-with-themes-a251e42b1451?source=rss----44c5ac415e14---4</link>
            <guid isPermaLink="false">https://medium.com/p/a251e42b1451</guid>
            <category><![CDATA[android-app-development]]></category>
            <category><![CDATA[ui]]></category>
            <category><![CDATA[fintech]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[ui-design]]></category>
            <dc:creator><![CDATA[Maxim Kostenko]]></dc:creator>
            <pubDate>Wed, 19 Jul 2023 11:14:44 GMT</pubDate>
            <atom:updated>2023-07-19T11:14:44.428Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*-OcAoMMf6MYIFYSj2wZxMA.png" /></figure><p>Keeping an app fresh and engaging is important to provide users with an enjoyable experience. One effective method is to add a personal touch to your app by supporting different themes in the application.</p><p>In this article, we’ll explore the various options that developers have to enhance user experience with the themes. We’ll also share our company’s experience in building themes, and the technical hurdles we faced during Android development.</p><p>Let’s dive in!</p><p>Depending on your app and company goals, you have several options to consider when implementing theming in your Android app. Here are a few:</p><ul><li><strong>Switching between light and dark mode:</strong> This has become almost a necessity on mobile platforms due to users’ preference for dark mode</li><li><strong>Implementing dynamic colours.</strong> Android 12 introduced <a href="https://developer.android.com/develop/ui/views/theming/dynamic-colors#:~:text=Dynamic%20Color%2C%20which%20was%20added,color%20in%20the%20wallpaper%20picker.">Dynamic Color</a>, allowing you to adapt your app’s colour scheme to match the user’s wallpaper</li><li><strong>Going above and beyond, as we did in Revolut:</strong> We offer all the options mentioned above, and also provide our customers with the ability to choose <strong>colour-based themes in the app</strong>. This allows them to select their favourite colour from a predefined palette, and the app’s appearance changes to match it</li></ul><p>By leveraging these options, you can create an app with a look and feel that aligns with your company’s branding and goals.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*p4Lb3w5fVRvTshjgvQG0sA.gif" /></figure><p>In this article, we’ll build a simple app that takes advantage of all the mentioned customisations options:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/320/0*K3xmLFBFhoMT72_T.gif" /></figure><h3>Switching between light and dark mode</h3><p>With many platforms now offering system-wide dark mode, implementing a dark theme in your app has become almost an industry standard. While there are numerous resources available for implementing dark mode, we won’t delve into all the technical details here.</p><p>That said, if you’re interested in learning more about implementing a dark theme in Android, you can refer to the <a href="https://developer.android.com/develop/ui/views/theming/darktheme">official guide</a>.</p><p>In our demo app, we support dark and light mode by defining two styles:</p><h4>Light:</h4><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/68c7ff182cdd7a8095299976aa1eee50/href">https://medium.com/media/68c7ff182cdd7a8095299976aa1eee50/href</a></iframe><h4>Dark:</h4><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/125c1be5073db1f47fcd44c7055a437d/href">https://medium.com/media/125c1be5073db1f47fcd44c7055a437d/href</a></iframe><p>As we put themes to the separate folders (values and values-night), the system will pick up the required theme automatically, depending on user settings.</p><p>Both styles inherit Theme.MaterialComponents.DayNight.NoActionBar, which provides support for light and dark modes for basic UI elements such as backgrounds and text. Additionally, we define our own colorPrimary and colorSecondary to give our app a unique look.</p><p>We use AppCompatDelegate.setDefaultNightMode to switch between light and dark mode in runtime:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/46ba41c61dbf783214e049549f6aaf98/href">https://medium.com/media/46ba41c61dbf783214e049549f6aaf98/href</a></iframe><p>Here is an example of how the app looks with support for both dark and light modes:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/320/0*NFKVoEEE-Y_7y62I.gif" /></figure><p>If you’re interested in seeing the full screen layout and code, you can check out the <a href="https://github.com/revolut-mobile/ColorThemesSample">demo project on GitHub</a>.</p><h3>Applying dynamic colours</h3><p>Android 12 introduces extended personalisation capabilities. Users can now choose a wallpaper, and the system will automatically adapt its colours to match the wallpaper. Additionally, apps can support wallpaper colours using the<a href="https://developer.android.com/reference/com/google/android/material/color/DynamicColors"> DynamicColors API</a>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*KFGR2MbHGV8pRe4A.png" /><figcaption>Demo from <a href="https://developer.android.com/develop/ui/views/theming/dynamic-colors#:~:text=Dynamic%20Color%2C%20which%20was%20added,color%20in%20the%20wallpaper%20picker.">official documentation</a></figcaption></figure><p>The simplest way to enable dynamic colours in an Android app is to use the Theme.Material3.DynamicColors.DayNight theme. But, using it requires migrating to the Material3 theme, which may not always be desirable because it not only changes the colours but also alters the appearance of components according to <a href="https://m3.material.io/">Material3 guidelines</a>.</p><p>If we want to support dynamic colours without changing the app’s theme, we can specify a dynamicColorThemeOverlay for our theme:</p><p><strong>Light theme style</strong></p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/9fb49adb667d1ffa736fc2d13b2d973a/href">https://medium.com/media/9fb49adb667d1ffa736fc2d13b2d973a/href</a></iframe><p><strong>Dark theme style</strong></p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/df8046cf528ce2a3a81c320e86cadff9/href">https://medium.com/media/df8046cf528ce2a3a81c320e86cadff9/href</a></iframe><p>To apply the styling, we need to call DynamicColors API in the Application’s onCreate:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/f7199428f00ba9eddf847a8caefd05da/href">https://medium.com/media/f7199428f00ba9eddf847a8caefd05da/href</a></iframe><p>And that’s how the app looks with dynamic colours enabled:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/320/0*NwoGqEofixoq12_y.gif" /></figure><blockquote>📢 <strong>Heads up!</strong> <strong>It’s crucial to follow Material Design naming conventions for the dynamic colours overlay to work properly.<em> </em></strong>The overlay applies a set of dynamic colours on top of the current theme and replaces the color tokens it is aware of, such ascolorPrimary and colorSecondary. You can learn more about material color tokens in the <a href="https://developer.android.com/develop/ui/views/theming/dynamic-colors#create-tokens">official doc</a>.</blockquote><h4>Changing colours in runtime</h4><p>With the approach described above, an app will always apply dynamic colours if they’re available.</p><p>But what if we want to have a more flexible way to work with themes? Some apps may want to maintain their branding by default and not rely on dynamic colours completely, giving users the option to select a theme they want.</p><p>At Revolut, we give our customers the ability to change the app’s theme at any time:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/320/0*b6oyuZm4iP87JQtQ.gif" /></figure><p>Although, this non-standard approach can be quite tricky to implement. After customers select a theme, we need to tell the app to recreate all the views, so they apply the new colours. The Android framework handles that for us when we need to change from the light to dark theme using AppCompatDelegate.setDefaultNightMode. This method will recreate activities under the hood.</p><p>But there is no way to force color changes to support switching to dynamic colors in the runtime. The good news is that nobody stops us from checking how AppCompatDelegate.setDefaultNightMode works and reproducing its approach:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/8af04df9a3e7da77f7706e26383cd29b/href">https://medium.com/media/8af04df9a3e7da77f7706e26383cd29b/href</a></iframe><p>Yes, this method simply loops through each activity and calls applyDayNight forcing activities to recreate! sActivityDelegates is a static set that collects all activity references.</p><p>The mentioned API is private, so to make our solution work we’ll draw inspiration from Android’s approach:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/eaf46336ab342e79b832cfbc5ec11f4c/href">https://medium.com/media/eaf46336ab342e79b832cfbc5ec11f4c/href</a></iframe><p>ColorThemesController registers ActivityLifecycleCallbacks to collect all activities to a single set. The applyColorTheme method will store a selected theme in the static field and trigger recreation for all the activities.</p><p>The most interesting part happens in onActivityCreated callback. It applies a selected theme to the newly created activity. DynamicColors.applyToActivityIfAvailable(activity) sets the dynamic colours. And for default theme, we’ll do nothing to keep default activity theme.</p><p>The logic starts working after we initialise ColorThemesController in the App class:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/4f1a25937db2dd5a1ba01546e0ad9c38/href">https://medium.com/media/4f1a25937db2dd5a1ba01546e0ad9c38/href</a></iframe><p>Now we can invoke ColorThemesController.applyColorTheme() to make a change in runtime:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/4caca36c4b40c111770d9647aea32951/href">https://medium.com/media/4caca36c4b40c111770d9647aea32951/href</a></iframe><p>Let’s check how that works in action:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/320/0*X631QrPWiJiUIPIv.gif" /></figure><p>You can find the full code of the app in the <a href="https://github.com/revolut-mobile/ColorThemesSample">GitHub repository</a>.</p><h3>Custom themes</h3><p>Dynamic colours are available on Android only, which leads to differences in personalisation capabilities between platforms if your app is distributed for iOS as well.</p><p>For those reasons, at Revolut, we didn’t stop at dynamic colour support. We also offer custom colour themes, allowing users to choose the main colour of the app:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/320/0*uo7VBwTdzzmYMW6O.gif" /></figure><p>It will be quite easy to modify our sample app setup to support a custom theme.</p><p>Let’s check how can we bring a custom <em>Orange</em> theme to the app.</p><p>First step will be to write a <em>theme overlay</em> which we’ll apply on top of the default theme. You can find more info on how theme overlays work in the <a href="https://medium.com/androiddevelopers/android-styling-themes-overlay-1ffd57745207">following article</a>. But in short, a theme overlay defines colours to replace in the given theme. It will add new colours to the theme if they are not present there. It’s like adding keys to the HashMap.</p><p>Here is a theme overlay for the light mode that replaces colorPrimary of our base theme:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/b8c4e2e92323aa5f0b91e98fbc5d645f/href">https://medium.com/media/b8c4e2e92323aa5f0b91e98fbc5d645f/href</a></iframe><p>And for the dark mode:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/0264348c1d05af670d5ac2b294ab893f/href">https://medium.com/media/0264348c1d05af670d5ac2b294ab893f/href</a></iframe><p>Now we’ll modify our ColorThemesController to support the new theme:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/e3441fb9a8deeede5ebb3992fe9de718/href">https://medium.com/media/e3441fb9a8deeede5ebb3992fe9de718/href</a></iframe><p>The Orange theme applied to activity using activity.theme.applyStyle(R.style.ColorThemeOverlay_Orange, true) . This call sets an overlay on top of the current activity theme, as we wanted.</p><p>Last touch will be to trigger ColorThemesController.applyColorTheme when orange theme selected:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/44d474610d2108b7a25ec0e2819c503a/href">https://medium.com/media/44d474610d2108b7a25ec0e2819c503a/href</a></iframe><p>Now our demo app is ready to switch between default, dynamic and the custom Orange theme, maximising customers personalisation capabilities.</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/0*K3xmLFBFhoMT72_T.gif" /></figure><h3>Conclusion</h3><p>In this article, we’ve explored a variety of ways to personalise your app’s appearance. By supporting dark mode, you can cater to the preferences of many users, or you can choose to use dynamic colours to make sure your app stays in sync with the system. Custom overlays offer cross-platform styles that are accessible to everyone.</p><p>Each app is one-of-a-kind, and every app has its own unique styling needs. With the right tools and techniques, you can create an experience that aligns with your app’s objectives and keeps users happy by offering a look and feel they enjoy. By implementing the approaches discussed in this article, you can make your app’s styling more adaptable and user-friendly.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a251e42b1451" width="1" height="1" alt=""><hr><p><a href="https://medium.com/revolut/customising-android-app-ui-with-themes-a251e42b1451">Customising Android app UI with themes</a> was originally published in <a href="https://medium.com/revolut">Revolut Tech</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Fundamentals of Android at Revolut]]></title>
            <link>https://medium.com/revolut/the-fundamentals-of-android-at-revolut-3537101a2c7c?source=rss----44c5ac415e14---4</link>
            <guid isPermaLink="false">https://medium.com/p/3537101a2c7c</guid>
            <category><![CDATA[fintech]]></category>
            <category><![CDATA[mobile-development]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[android-app-development]]></category>
            <dc:creator><![CDATA[Revolut]]></dc:creator>
            <pubDate>Fri, 17 Mar 2023 09:48:42 GMT</pubDate>
            <atom:updated>2023-03-23T14:33:59.022Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="The Fundamentals of Android at Revolut" src="https://cdn-images-1.medium.com/max/1024/1*ZzTMRMa8CC0bKEkWnagGnQ.png" /></figure><p>People deserve more from their money. More visibility, more control, more freedom. Since 2015, Revolut has been on a mission to deliver just that. With an arsenal of awesome products that span spending, saving, travel, transfers, credits, crypto, investing, exchanging, and more, our mobile super app has helped 25+ million customers send, spend, save, and invest smarter.</p><p>Let’s take a look behind the scenes of the Android development at Revolut and learn how we deliver our iconic apps to millions of customers all over the world.</p><h3>Dream team</h3><p>Our Android team is one of the best in the industry — and we’re proud of it. It’s one of our driving forces: over a hundred excellent Android engineers work on our key products, such as Revolut for personal customers, Revolut Lite, Revolut Business, and Revolut &lt; 18 for teens.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*-PUZRKw4M3BN_cru" /></figure><p>At Revolut, we believe that building diverse, lean teams of brilliant go-getters who break down barriers, is the key to winning. That’s why we have around 70 strong teams that split their work across two main streams: Platform and Product.</p><p>The Platform stream consists of Design System and Mobile Platform teams, while the Product stream includes various product teams, such as the Onboarding team, Payments team, Trading team, Crypto team, Credit team, and more. Each team is made up of Product Owners, Designers, iOS, Android, Backend, and Frontend engineers who work together to deliver exceptional results.</p><p>To better understand our teams and how they contribute to our success, let’s take a closer look at their roles and responsibilities.</p><h3>Mobile Platform Team 👨‍🔧</h3><p>Our Mobile Platform team is involved in a broad range of areas, such as building infrastructure, managing releases, maintaining architecture, and owning internal tools and libraries.</p><p>The team upgrades projects configuration, particularly improve performance of Gradle Builds, maintains architecture, manages dependencies and keeps architecture sustainable, modernises CI/CD pipeline, facilitates mobile release process, creates internal tools and frameworks (Automated Analytics tracking tool, Automated UI tests framework, DI declaration framework etc.), enhance app performance, and increase security.</p><h3>Design System Team 👨‍🎨</h3><p>The Design System team is the heart of our design culture. It’s responsible for building and maintaining the central source of truth for the developers, our UIKit (library of reusable components and guidelines).</p><p>They create high quality UI components, keep the designs consistent across the apps, customize themes and styles, optimise views, layouts, and overall rendering performance. They constantly improve user experience, create unified visual interfaces, ensure smooth transitions, and make our UI/UX not just convenient but also enjoyable.</p><h3>Product Teams 👩‍💻</h3><p>The Product teams play a critical role in every step, stage and release of a product. They’re like small startups; self-led, with the freedom to innovate and develop new products and features.</p><p>Such a team is focused on building a particular product, gathering requirements, implementing new features, collecting customer feedback, and making improvements. Each of our products, including Credit, Crypto, Stays, Rewards and others, build our one app for all things money.</p><h3>Development process</h3><p>The development process requires participation of all team members to achieve their goal. Planning, visibility, and structure in the development process, are integral components that allow the team to remove stress and implement quality functionality that satisfies business needs on time.</p><p>The process starts with the definition of requirements and acceptance criteria, which are validated and challenged by the team. Once requirements are clear, the scoping phase can begin. Scoping phase is covering an analysis of the requirements, identifying potential solutions, and building development plans.</p><p>After the scoping phase, all team members should be aligned, so the development itself can start. That said, new ideas or challenges may appear during the development process, meaning the already defined solution may need to be re-visited..</p><p>We adhere to Agile philosophy, and each team has a choice which Agile project management systems to use, usually a Scrum and Kanban, or a mix.</p><h3>Project level architecture</h3><p>All our projects are multi-module, meaning we organise our features as separate modules. At the same time, we pay attention to granularity level, and tend not to separate features that have high cohesion and are linked to the same domain area, because we know that modules that are too fine-grained make the overhead a burden.</p><p>Not all our modules are feature modules. There are Core modules for common needs, like network, databases, or android components, UIKit modules for design elements, Models modules for basic data structure, and Test modules. We reuse common components and basic functionality between feature modules in one project, and share it across our projects. It’s also applicable to features: we have several feature modules that we share across different products.</p><figure><img alt="Dependency graph of multi-module Revolut project" src="https://cdn-images-1.medium.com/max/1024/0*wznS90RoFb9B2K-7" /><figcaption>Dependency graph of multi-module Revolut project</figcaption></figure><p>In case of feature modules, we follow an Api/Implementation approach, so each feature is represented as a lightweight Api module and a heavy Implementation module.</p><p>According to this approach, an Implementation of a feature should depend only on Api modules. This way, heavy Implementation modules don’t depend on each other, and can be built in parallel, to speed up the build time. This approach also makes a feature implementation less fragile, since it depends only on stable Api modules.</p><h3>App level architecture</h3><p>All our products are implemented with Clean Architecture principles. Revolut Business and Revolut &lt; 18 are based on our own <a href="https://github.com/revolut-mobile/kompot">Kompot framework</a>. Kompot implements the MVVM design pattern, provides unidirectional data flow, simplifies navigation between different parts of an app, and supports multi-module from the box.</p><p>But it’s not the only one: Revolut for retail customers, the oldest and biggest app, follows a different approach based on MVP design pattern. That said, the new approach with Kompot can also be used.</p><h3>UI/UX</h3><p>The way we work with UI deserves special attention, and we mostly build our screens atop the RecyclerView.</p><p>Our UIKit defines a set of design components that can be used for building UI. Most of these components are represented in our code as<a href="https://revolut.atlassian.net/wiki/spaces/BD/pages/43041320"> </a>RecyclerViewDelegates, and we build each screen as a list of these premade components (delegates). You can find more details about <a href="https://medium.com/revolut/building-new-features-using-recyclerview-only-aa7db3d8f282">how we recycle our screens</a>.</p><p>We also pay extra attention to UX. According to our guidelines, screens should support Loading/Error/Empty states. In addition, transition between states should be smooth, without any visual noises. We also tend to render all content that’s already loaded, and only use shimmer skeletons for parts that are on the way, instead of blocking the whole screen with old-fashioned progress bars.</p><p>More importantly, we strive to show content on a screen as fast as possible, regardless of poor internet connectivity.</p><p>Here, <a href="https://medium.com/revolut/reactive-data-flow-in-revolut-android-app-30a49fa1572e">Reactive data flow</a> enters the scene. According to this approach, a repository provides a stream of the Data, containing typed content, a flag indicating that we’re loading content from the network, and optional error.</p><p>The repository also defines logic regarding content retrieving and storing. Usually, the content we get from remote sources is stored in local cache, so when we subscribe to a Data stream, our repository emits cached content at first, so we can avoid loading, and show screen state immediately. We store information in memory and persistent storage, so our customers have access to their financial data from everywhere, even in offline mode.</p><h3>Tech stack recap</h3><p>We write in Kotlin and tend to use Kotlin coroutines and flows, but RxJava 2 will probably always be part of our heart and code base too.</p><p>We also use:</p><ul><li><strong>Frameworks</strong>: RxJava 2, Coroutines (Flow), OkHttp, Retrofit, Dagger 2, Facebook Screenshot tests, Kaspresso UI tests, Android SDK</li><li><strong>Database</strong>: SQLite (Room OM)</li><li><strong>Infrastructures</strong>: Bitbucket, Jira, TeamCity</li><li><strong>Utilities</strong>: Charles, Postman</li></ul><h3>QA &amp; Testing</h3><p>At Revolut, we don’t have QA engineers, but it doesn’t mean that we don’t verify our app’s correctness, or don’t identify possible issues, before releasing it to the public. We have several types of tests that help us consistently verify functional behaviour. Unit tests, Integration tests, Screenshot tests, and automated UI tests, allow us to get quite comprehensive coverage.</p><p>Additionally, our release lifecycle consists of several stages. In the first Alpha phase, we test our new products and features internally to make sure that the product meets business requirements and functions correctly without major bugs.</p><p>The following phase is Beta, where we publish beta app versions, used by external beta users, to help us identify bugs, and performance or usability issues, at an early stage, before public release.</p><p>Last but not least, we cover all major changes and updates in the code by feature toggles, managed by our internal release management system, Vader. This allows us to roll out features gradually and safely, and turn the feature off in case there’s an error.</p><h3>Code review &amp; Git flow</h3><p>Developers create a new branch for any functional changes, rather than committing directly onto their local main development branch. After updating, committing, and pushing changes to the corresponding branch, developers open a Pull Request to discuss and review the changes before they go to the development branch.</p><p>When a Pull Request is opened, or any changes to the remote tracked branch appear, we perform CI/CD pipelines, including project building, running Unit and a set of most fragile UI tests, executing Danger verifications.</p><p>Danger is a tool that helps automate common code review routines, allowing us to create and share automated messages that remind developers of common mistakes, so they don’t run into them time and time again.</p><p>The Pull Request is ready to merge only if the project is successfully built, Unit and UI tests are passed, there’s no error from the Danger side, and the Pull Request has at least two approvals. All our code, modules, classes and tests are assigned to particular code owners (teams), allowing us to find owners of the code easily while developing, and more importantly, to assign reviewers, whose code has been changed to the Pull Request.</p><h3>Release management 🚝</h3><p>Our Release train departs each Tuesday at 9:00 am. At this moment, the automated code freeze happens, where all changes from the development branch go to the release branch. By this time, all involved teams make sure everything is ready, and check corresponding feature toggles. Starting from this moment, only critical fixes can be added to the release branch.</p><p>When a freeze has happened, we prepare a Google Play bundle, running Unit, and all UI tests. If everything is fine, we publish the first beta release. (We go through these steps and upload beta releases every day but Sunday.) At this stage, we collect the first beta customer feedback, and check analytics and crash reports.</p><p>On the following Tuesday, if all goes well, we merge the release branch to the store branch, and perform a staged public rollout of the release version. By this time, our release train transforms into a rocket, delivering a new version of the app to millions of users around the world, and a new release train comes into the station.</p><p>This is the last step of the app release cycle, but not necessarily a feature release. Even when a rocket is far away in space, and our new version of the app already on customer devices, we can manage rollout and availability of the particular feature remotely, using feature toggles powered by Vader.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*WK1FmBh3twZx4c19" /></figure><h3>Analytics</h3><p>We analyse customer data to create personalised models and enhance the customer experience. For instance, we categorise events to personalise the main screen for each customer, and offer features based on their behaviour.</p><p>To protect our customers’ privacy, we do not use third-party analytics tools. Instead, we’ve built an in-house UI analytics solution that includes both a frontend framework and a server. The frontend framework tracks user interactions with the app out of the box, without requiring developers to write any ad-hoc code. The server captures batched events of screens appearing and buttons being clicked, allowing us to analyse the user journey and behaviour to create a better experience for our customers.</p><h3>Join us</h3><p>If you’re excited by our processes, how we work, and the opportunities you could get from working with us, why don’t you <a href="https://www.revolut.com/careers">check out our Careers page</a> and explore openings in our Android team?</p><p>Credits go to Revolut Android team, especially <a href="https://medium.com/u/ea8e1a255560">Anton Nazarenko</a>, <a href="https://medium.com/u/d87145858bc3">Nikita Barishok</a>, <a href="https://medium.com/u/396a396099bd">Akhmetov Kirill</a> and <a href="https://medium.com/u/ce818245985">Artur Kharchenko</a>, Head of Engineering <a href="https://medium.com/u/cc68dab1c764">Ivan Vazhnov</a> and our CTO <a href="https://medium.com/u/fa6c099e078f">Vlad Yatsenko</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=3537101a2c7c" width="1" height="1" alt=""><hr><p><a href="https://medium.com/revolut/the-fundamentals-of-android-at-revolut-3537101a2c7c">The Fundamentals of Android at Revolut</a> was originally published in <a href="https://medium.com/revolut">Revolut Tech</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Security Drone: scaling continuous security at Revolut.]]></title>
            <link>https://medium.com/revolut/security-drone-scaling-continuous-security-at-revolut-862bcd55956e?source=rss----44c5ac415e14---4</link>
            <guid isPermaLink="false">https://medium.com/p/862bcd55956e</guid>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[application-security]]></category>
            <category><![CDATA[security]]></category>
            <category><![CDATA[fintech]]></category>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[Krzysztof Pranczk]]></dc:creator>
            <pubDate>Tue, 03 Jan 2023 09:24:53 GMT</pubDate>
            <atom:updated>2023-01-03T09:24:53.727Z</atom:updated>
            <content:encoded><![CDATA[<h3>Security Drone: Scaling Continuous Security at Revolut</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*eBMRBk4m8s4UjQZYNKosvQ.png" /></figure><h3>Intro</h3><p>As we’re continuously growing and extending our product offerings, we can face many technological challenges. These challenges are solved by our engineers and this results in many features, changes and updates being developed and successfully delivered to our customers. However, with the development of new features comes many security challenges that are faced by the internal <strong>Application Security Team</strong>.</p><p>This lovely bunch is responsible for the security assurance of every new feature developed by our engineers. To provide the highest level of security assurance to our products, we’ve implemented a number of processes placed in different stages of the Software Development Life Cycle (SDLC), including automated scans in our CI/CD pipelines. However, it wouldn’t be possible to efficiently triage every security finding produced by automated scanners. In July 2022, there were nearly 39,000 commits created by over 900 authors! To address this challenge, we developed <strong>Security Drone</strong>. In this article, we’d like to share with you our approach to provide the highest security assurance in fast CI/CD environments.</p><h3>Challenges faced by Revolut</h3><p>The classic approach to security testing requires the security teams to perform a manual review of any developed features, with the help of automated security scans. Traditionally, this work was executed on a risk and priority basis and that need was stretching the team beyond its capacity. Additionally, the team had to cope with the numerous CI pipelines across the company. This approach wasn’t a viable solution in terms of scaling, quality and coverage.</p><p>Some of you may have an idea of the security challenges faced in a fast CI/CD environment. If not, let us make a little recap of the challenges we’ve been facing:</p><ol><li>Software changes are constantly increasing</li><li>New changes are integrated and deployed every day</li><li>Engineers tend to prioritise the development of functionalities over security</li><li>The internal application security team can’t be big enough to have a dedicated security engineer for each project — both internal and external</li><li>AppSec teams nowadays must work on automating the work they were doing 10 years ago in a manual way</li><li>With more tools integrated into the pipelines, the timeline of each job increases, which negatively affects the development experience</li><li>And many more challenges you probably observe in your companies too!</li></ol><h3>First solution — The classic approach to CI/CD pipeline scans</h3><p>Our first solution, and the most simplistic one, was to onboard automated security scanners like Static Application Security Testing (SAST) and Software Composition Analysis (SCA) and review the findings within the AppSec team. It worked, but we started facing another problem: the company was growing, and from an initial handful of projects, now we had to deal with thousands of projects and non-stop builds at any given time. As a result, we had to manage hundreds of CI pipelines used for security purposes.</p><p>Let’s see some numbers of what we observed in our environment. Every 24h of a working day were about<strong> 950 new pull requests</strong> (PR) with nearly<strong> </strong>1.85<strong> </strong>commits<strong> </strong>per PR. During working hours, automated scans were executed 3–4 times per minute, on average, against various projects. The chart below presents how many security scans were performed on the 14th of July 2022 every 30 minutes.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*mFtYyWvv-tLPjuSu" /><figcaption>Number of automated security scans per 30minutes</figcaption></figure><p>With those numbers, we faced another challenge — triaging all of the security findings. These scans produced a high number of false positive vulnerabilities that had to be manually triaged by the security team.</p><p>Initially, we didn’t think scanning every software change was the way to go, we should only be scanning the changes intended for the Production environment. We did the analysis and concluded that about<strong> 81%</strong> of the commits had a final destination to the main branch, and we were facing the same challenge. Our scanners would be completing at least three successful security scans on a software change every minute!<strong> </strong>This amount of scans would still result in a potentially high number of false positives, which had to be reviewed and triaged within a certain period of time. Not only that, but more and more security scanners within CI/CD pipelines would affect their timelines negatively.</p><p>So we thought: ‘Do we really have to go in this direction? Do we have to manage all of the pipelines for various projects and triage all of the identified security issues within the AppSec Team? Do we have to affect CI/CD timelines negatively?’</p><p>At this point, we had a lot of doubts and questions about this approach. In the Application Security team we understood this approach wouldn’t be scalable in a fast-paced environment such as Revolut. We could implement some small improvements to address each of these questions. However, those improvements could just delay bigger problems for later on. At this point, we decided to go in a slightly different direction, having a security-shift-left approach in our minds. Security static analysis tools can easily be shifted left to an earlier phase in the SDLC, since these types of tools don’t require the application to be running.</p><h3>Second solution — Security Drone</h3><p>You may expect as a second solution something extremely smart and well-designed. So did we. But it didn’t take long for us to take an agile approach with lean implementations. We also started security shifting left and communicating security issues to developers as early as possible, before going into testing or production environments.</p><p>At the beginning of our MVP solution, we decided to scan code changes during the pull request phase. In our opinion, it was a natural step for engineers to propose software changes for their colleagues to approve. At this point, we also decided to communicate all of the identified security findings to developers. The scanner’s results could be taken into account during code review, which is an integral part of the development process in Revolut. It should be noted that our scanner was triggered when a new PR was created or code was updated in an already existing PR. We also decided to place Security Drone in a Kubernetes cluster to scan the code independently from CI/CD pipelines and have a centralised scans management. In many cases, independent scans were able to deliver results to developers faster than CI jobs were finished.</p><p>Initially, we implemented only a SAST scanner solution to make the process as fast as possible, to quickly deliver results to developers. We aimed to provide results in under 300 seconds. We carefully researched available SAST solutions to choose the most suitable one for our needs. It had to be fast, well-documented and allow us to write custom rules to identify potential security issues specific to our environment. Last but not least, we wanted to achieve the lowest possible false positive rate to avoid producing irrelevant findings, to limit the amount of manual work that we had to cope with during triaging. But not only that, as we had decided to communicate all of the identified security findings to developers at the PR page, we had to make sure not to affect their experience negatively by reviewing a number of irrelevant findings.</p><p>Later, we started implementing new scanners in Security Drone such as Software Composition Analysis and Infrastructure as Code to bring more value to the automated security testing. Security Drone’s high level architecture is presented below.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/411/0*iAcFdlgGFLRhPEty" /><figcaption>Architecture of Security Drone</figcaption></figure><p>Currently, we scan all pull requests created by Revolut engineers. In July, Security Drone performed over 39,000 scans. Median scanning time is below 112 seconds and the average is below 110 seconds. Initially, we used 19 SAST and 63 IaC rules. Only high and critical SCA issues were directly reported to our developers. Our first MVP was released in Q1 2022, extended and adjusted in the last months. Now, Security Drone has been operating in production for the last eight months without issues on both the operational and engineering distribution sides.</p><p>We use the following tools in Security Drone:</p><ul><li><a href="https://semgrep.dev/">Semgrep</a> — Static Application Security Testing</li><li><a href="https://docs.snyk.io/products/snyk-open-source">Snyk Open Source</a> — Software Composition Analysis</li><li><a href="https://www.checkov.io/">Checkov</a> — Infrastructure as a Code</li></ul><p>What have we achieved with Security Drone?</p><ol><li>We have adopted a shift-left approach to security to identify and communicate security findings earlier in the SDLC, before going into testing or production environments</li><li>Security issues can be fixed before going into production, and as a result, they don’t have to be manually triaged by AppSec Team members</li><li>Only merged security issues are reported to the AppSec Team to triage and loop into the vulnerability lifecycle process</li><li>We lowered the false positive rate by carefully choosing the SAST solution and continual tuning of rules. We were able to achieve ~3.8% FP rate!</li><li>Our centrally managed scanner currently scans 100% of the code in Revolut which saves us hundreds of hours of manual reviews. Here are some numbers from last 24 hours:<br> • Nearly 1700 pull requests were scanned<br> • Over 3900 scans associated with above PRs were performed</li><li>Ability to find new vulnerabilities in other applications based on patterns</li><li>The scans are fast and don’t disrupt the developer experience. They’re executed in parallel and scanning times are presented below:<br>• Median scanning time for SAST is 11 seconds<br>• Median scanning time for IaC is 22 seconds<br>• Median scanning time for SCA is 101 seconds</li><li>Increased security awareness and continuous learning amongst engineers. They’re also aware of the direction that AppSec is moving.</li></ol><h3>What is next?</h3><p>Security Drone will always be under development as new technologies are emerging and improvements to the development experience can be made. On our roadmap we have various points, some of which include:</p><ul><li>Ability to flag findings as a false positive in a developer-friendly way</li><li>Incremental SAST scans — scan only code changes in PRs</li><li>Integration of more security scanners and the development of more SAST/IaC rules</li></ul><p>Keep your eyes peeled for our next blogpost around Application Security in Revolut, as we may share more interesting tools and guides on how we solve the challenges we face every day.</p><h3>Credits</h3><p>Credits go to every Revolut AppSec engineer involved in the design and development of Security Drone, especially:</p><p><strong>Arsalan Ghazi, Krzysztof Pranczk, Pedro Moura, Roger Norton</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=862bcd55956e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/revolut/security-drone-scaling-continuous-security-at-revolut-862bcd55956e">Security Drone: scaling continuous security at Revolut.</a> was originally published in <a href="https://medium.com/revolut">Revolut Tech</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[iOS Screen Navigation Engine at Revolut]]></title>
            <link>https://medium.com/revolut/ios-screen-navigation-engine-at-revolut-1b189f45240?source=rss----44c5ac415e14---4</link>
            <guid isPermaLink="false">https://medium.com/p/1b189f45240</guid>
            <category><![CDATA[ios-app-development]]></category>
            <category><![CDATA[fintech]]></category>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[ios]]></category>
            <category><![CDATA[ios-engineering]]></category>
            <dc:creator><![CDATA[Vincent Berihuete]]></dc:creator>
            <pubDate>Wed, 17 Aug 2022 11:35:30 GMT</pubDate>
            <atom:updated>2022-11-24T15:54:42.658Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xUjChnqjMQ6eMEUkBpG5Tg.png" /></figure><p>Ever heard about the Coordinator pattern? Good for you, this article isn’t about it. I’m here to open your eyes to Custom Navigation Engines just like the one we created on our Revolut <strong>Flow Engine.</strong></p><p>First let’s talk about some traditional approaches for navigation before we get into our Flow engine:</p><p><strong>Storyboard actions navigation</strong></p><figure><img alt="Three screens, the first one being a navigation view, the second one being a main sample screen with a button to navigate to the third screen." src="https://cdn-images-1.medium.com/max/1024/1*nwp51Jk-UEje1yOLiencbA.png" /></figure><p>The most traditional way of doing navigation with storyboards is the one that’s triggered by an action. As you know, you can ctrl + drag from your button to reach the desired screen on your storyboard.</p><p><strong>Segue driven navigation</strong></p><figure><img alt="Four screens shown, first one being a navigation controller the second one the main sample screen the third one shows a button navigation and the fourth one shows a segue assignation by a line drag" src="https://cdn-images-1.medium.com/max/1024/1*NtB3dDdNeFQ8c9PurlsvjQ.png" /></figure><p>Another traditional type of storyboard navigation are segues, by doing ctrl+drag from one view controller to another we create a connection of a certain type (push, modally, etc) and assign it an ID.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/8976cef3dea76b0581556b979848d899/href">https://medium.com/media/8976cef3dea76b0581556b979848d899/href</a></iframe><p><strong>ViewController code driven navigation</strong></p><p>The most traditional way outside storyboards, is to instantiate and push or present your view controllers programmatically.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/efae70b3641c09ff15c404f46504a84e/href">https://medium.com/media/efae70b3641c09ff15c404f46504a84e/href</a></iframe><h3>Flow Engine</h3><p>Now let’s bring out the big guns: the abstraction of navigation outside view controllers.</p><p><em>Why do we need to abstract navigation</em>? Well, imagine you have a <strong>FoodDeliveryApp</strong> and in there you have a screen called <em>ProductDetails</em> that shows the information for a given product (Hamburger 🍔, fish and chips 🍟, etc) — but you want to call this screen and its dependencies from anywhere. Will it be enough to just instantiate the screen everywhere, with its dependencies and its actions? 🤯 of course not! 😅</p><p>Wait, what? Don’t worry, here’s some code:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/b24d2268c0b542848c5d8a6f05379a6e/href">https://medium.com/media/b24d2268c0b542848c5d8a6f05379a6e/href</a></iframe><p>When we want to call our ProductDetail screen and pass its business actions through dependency injection, we end up doing this:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/cb3e85b0c7ae83eb4080db9b221086f3/href">https://medium.com/media/cb3e85b0c7ae83eb4080db9b221086f3/href</a></iframe><p>Code looks clean to you? Well, kind of 😅. What happens when you need to show that product page from other view controllers and<em> handle its actions</em>? .showRelatedProduct and .addToOrder? Copy and paste this implementation? 😢 Of course not!</p><p>What if you could abstract this so you only call some magic method? Let’s say runProductDetailsFlow(productId:_) with actions and dependencies of this screen abstracted in that layer? Well, without further ado:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*KXjc2_tkP7OElyE4CPGiyQ.png" /></figure><p>Basically as the name says, it’s an engine (state machine) to handle a flow (set of steps). A carefully crafted state machine created by our own Revolut Legend <a href="https://medium.com/u/b5c5b507cb45">Ilya Velilyaev</a>. Let’s start with the backbone of it:</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/831c9c52c15a3d440ad759795599b840/href">https://medium.com/media/831c9c52c15a3d440ad759795599b840/href</a></iframe><p>After having your FlowEngine (feel free to copy and paste that file, and use it to create great flows in your apps 🚀), you can create Flows for your different business cases. Let’s go back to our FoodDelivery app example and create a flow to show ProductDetail with its dependencies and business actions abstracted! 🎉</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/c8977500a2fc329eea9772578fb10234/href">https://medium.com/media/c8977500a2fc329eea9772578fb10234/href</a></iframe><p>Once you create and define yourFlow with its steps and initial State, you need to create aFlowPerformer — the one who will actually execute these steps and their logic.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/6118ae521d88deedea4de5a39e26a172/href">https://medium.com/media/6118ae521d88deedea4de5a39e26a172/href</a></iframe><p>When you have your FlowPerformer you can proceed to create your magic maker FlowRunner — this is the one that allows you to call as one liner: flowRunner.runProductDetailFlow(...) :</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/ab45ee95690003beb6fd6cf278eb119c/href">https://medium.com/media/ab45ee95690003beb6fd6cf278eb119c/href</a></iframe><p>Finally from anywhere in your code (most preferably from other flows), you can call as many flows as you like while avoiding code duplication and have an easier <a href="https://en.wikipedia.org/wiki/Test-driven_development">TDD</a> approach to your code.</p><iframe src="" width="0" height="0" frameborder="0" scrolling="no"><a href="https://medium.com/media/fcb0669a294a48b452a31d11c6600b50/href">https://medium.com/media/fcb0669a294a48b452a31d11c6600b50/href</a></iframe><p>That’s it, hope you enjoyed and that you start controlling, testing and abstracting your navigation in your apps 🚀</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1b189f45240" width="1" height="1" alt=""><hr><p><a href="https://medium.com/revolut/ios-screen-navigation-engine-at-revolut-1b189f45240">iOS Screen Navigation Engine at Revolut</a> was originally published in <a href="https://medium.com/revolut">Revolut Tech</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>