<?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[ASOS Tech Blog - Medium]]></title>
        <description><![CDATA[A collective effort from ASOS&#39;s Tech Team, driven and directed by our writers. Learn about our engineering, our culture, and anything else that&#39;s on our mind. - Medium]]></description>
        <link>https://medium.com/asos-techblog?source=rss----6757df096022---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>ASOS Tech Blog - Medium</title>
            <link>https://medium.com/asos-techblog?source=rss----6757df096022---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 05:44:36 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/asos-techblog" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Android Clean Architecture: Understanding the Data Layer]]></title>
            <link>https://medium.com/asos-techblog/android-clean-architecture-understanding-the-data-layer-a8df03b508c2?source=rss----6757df096022---4</link>
            <guid isPermaLink="false">https://medium.com/p/a8df03b508c2</guid>
            <category><![CDATA[apps]]></category>
            <category><![CDATA[android]]></category>
            <category><![CDATA[android-app-development]]></category>
            <category><![CDATA[clean-architecture]]></category>
            <category><![CDATA[mobile-app-development]]></category>
            <dc:creator><![CDATA[Sean Coyle]]></dc:creator>
            <pubDate>Tue, 04 Nov 2025 09:10:36 GMT</pubDate>
            <atom:updated>2025-11-06T09:31:14.334Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="Header image showing the Android logo cleaning" src="https://cdn-images-1.medium.com/max/1024/1*pnIsXk3LvpVeBNyzZoG0XA.png" /><figcaption>Image generated by DALL-E</figcaption></figure><p>In modern Android development, Clean Architecture offers a structured, flexible approach to building applications that are scalable, maintainable and testable. The Data Layer plays an important role in abstracting the complexities of networking, databases, and caching, ensuring that the Domain &amp; Presentation Layer remain agnostic to the underlying data sources.</p><p>🔗 <a href="https://medium.com/asos-techblog/android-clean-architecture-understanding-the-domain-layer-28bf81d83ef7">Our previous discussion on the Domain Layer</a> outlines the principles this article will build upon. Here, we’ll examine the structure of the Data Layer, exploring its key components, outline best practices for implementation, and discuss how to effectively integrate it into your Android projects.</p><h3>What is the Data Layer?</h3><p>The Data Layer is in charge of all the behind the scenes work when it comes to handling data in the application. Whether it’s fetching from a remote API, reading from a local database, or managing an in-memory cache, this layer does the heavy lifting. Its job is to provide data in a consistent way so the rest of the app doesn’t have to worry about where it came from or how it was stored.</p><p>In Clean Architecture, a key rule is that the Data Layer shouldn’t be accessed directly by the Presentation Layer (like ViewModels or UI components). Instead, it talks only to the Domain Layer, which defines the business rules that the application relies on. This separation keeps things modular and flexible, so if you ever need to switch remote APIs or change the storage setup, you can do it without affecting the rest of the application.</p><figure><img alt="A diagram which depicts the three layers in Clean Architecture. The Presentation Layer, the Domain Layer and the Data Layer." src="https://cdn-images-1.medium.com/max/1024/1*INX-TUwdPXyR1bzMsEoPyQ.jpeg" /><figcaption>Dependency Arrangement in Clean Architecture</figcaption></figure><h3>Anatomy of the Data Layer</h3><h4>Data Sources</h4><p>Define how data is retrieved, whether from remote APIs, databases, or in-memory caches. They are often categorised into RemoteDataSources and LocalDataSources.</p><h4><strong>Repositories</strong></h4><p>Serve as the entry point for data access, acting as the interface between the Data Layer and the Domain Layer. Repositories orchestrate data from remote or local Data Sources.</p><h4><strong>Remote API Interfaces</strong></h4><p>Define the contract for network communication. They specify HTTP methods (like @GET, @POST), endpoint paths, query/path parameters, headers, and the expected response models. This ensures consistent and type safe interaction with remote APIs.</p><h4><strong>Database Access Objects (DAOs)</strong></h4><p>Provide an abstraction layer over the database. They define the SQL queries and data operations used to perform CRUD actions (Create, Read, Update, and Delete).</p><h4><strong>Data Transfer Objects (DTOs)</strong></h4><p>Are structured representations of external data like REST API responses.</p><h4><strong>Entities</strong></h4><p>Are database representations that directly map to tables. They define the schema of the database.</p><h4><strong>Mappers</strong></h4><p>Transform data between DTOs, Entities, and Domain Models. Mappers are necessary to avoid direct dependencies between the Data and Domain Layers.</p><figure><img alt="A diagram showing a representation of data flow through the Clean Architecture layers starting in the Presentation Layer with a Fragment, moving to the ViewModel then crossing into the Domain Layer with a Use Case. The Use Case then calls the Repository Interface. We then cross into the Data Layer into the Repository Implementation. From here we can either go to a Local Data Source or a Remote Data Source to send or fetch information." src="https://cdn-images-1.medium.com/max/1024/1*pZatdH0cAp8y3hT9ZVc8Yg.jpeg" /><figcaption>Representation of Data Flow in Android Clean Architecture</figcaption></figure><h3>Key Responsibilities of the Data Layer</h3><p>✅ <strong>Fetching, Persisting and Caching Data</strong><br>The Data Layer handles CRUD operations from remote data sources such as REST APIs, and can persist data locally using tools like Room (for databases), DataStore (for key-value pairs), or in-memory caching (for temporary data) when needed.</p><p>✅ <strong>Handling Network Responses</strong><br>The Data Layer processes and validates network responses. It ensures data from external sources like REST APIs is valid, complete, and in the correct format before it is passed on to the Domain Layer.</p><p>✅<strong> Retry Logic &amp; Failure Handling</strong><br>The Data Layer handles unreliable network conditions by implementing retry strategies like exponential back-off and gracefully managing failures to avoid disrupting the user experience.</p><p>✅ <strong>Logging &amp; Error Reporting</strong><br>When operations fail, capturing those failures at the source is important. The Data Layer should log exceptions and optionally report them using crash reporting tools for easier debugging and analysis.</p><p>✅ <strong>Threading<br></strong>Libraries like Retrofit, Room, and DataStore automatically handle threading when using Coroutines, ensuring data operations do not block the main thread. If using a different library the responsibility of switching threads for I/O work belongs to the Data Layer.</p><p>✅ <strong>Mapping DTOs &amp; Entities to Domain Models<br></strong>Transforming raw data (like REST API responses or database entities) into usable domain models is a key responsibility of this layer. This separation helps maintain a clear boundary between external data formats and the applications business logic.</p><p>✅ <strong>Ensuring Data Consistency</strong><br>Data Sources should return immutable data to ensure consistency and prevent race conditions or data corruption. Immutable data guarantees that the data passed to the higher layers of the application is reliable.</p><p>✅ <strong>Data Security &amp; Privacy</strong><br>While security is a shared responsibility across layers, the Data Layer plays an important role. Whether encrypting local storage or ensuring secure data transmission over the network, this layer helps protect user data and privacy.</p><p>❌ <strong>Should Not Contain Business Logic</strong><br>The Data Layer focuses on data management, not decision making. Business logic should reside within the Domain Layer to maintain separation of concerns and maintainability.</p><p>❌ <strong>Should Not Be Accessed Directly by the Presentation Layer<br></strong>Access to the Data Layer is restricted via the Domain Layer to ensure business logic is consistently applied and to prevent inconsistent Data Layer access throughout the application.</p><blockquote>💡 In smaller projects where a Domain Layer is omitted, this boundary might be more relaxed. But in scalable apps, adhering to this separation leads to a more maintainable code base.</blockquote><h3>Repository Pattern in Clean Architecture</h3><p>Repositories act as the middle layer between the Domain Layer and Data Sources (like REST APIs or local databases). Their job is to expose a stable, domain focused interface that hides the underlying complexity of how and where data is fetched or stored.</p><p>Typically they do <strong>not</strong> handle mapping, threading, or low level error handling. Those concerns live in the remote and local Data Source classes. However, if your application doesn’t use Data Source classes (smaller projects) it’s acceptable for the Repository to take on these responsibilities. Repositories focus on<strong> </strong>orchestrating data between sources (remote &amp; local), and deciding how to combine or prioritise data before returning the result to the Domain Layer.</p><h4>1. Naming Repositories</h4><p>Repositories should follow the format:</p><blockquote><strong><em>[Type of Data] </em></strong><em>+ Repository</em></blockquote><p>For example:<br>✅ <strong>Bag</strong>Repository<br>✅ <strong>Checkout</strong>Repository</p><h4>2. Key Responsibilities of Repositories</h4><p>✅ <strong>Data Orchestration</strong><br>Repositories coordinate between multiple data sources. For example, fetching from local cache first and falling back to remote if needed (or vice-versa). They decide where to get the data from, not how to get it.</p><p>✅ <strong>Return Domain Models</strong><br>Repositories always expose Domain Models to the Domain Layer. Any mapping from DTOs or database Entities happens in the remote or local Data Source classes. If your application omits Data Source classes the Repository can take on the responsibility of mapping.</p><p>✅ <strong>Main Thread Safety</strong><br>Repositories shouldn’t perform long running operations on the main thread. But they also shouldn’t be responsible for threading. If thread shifting is required that responsibility belongs to the Data Source performing the operation. If your application omits Data Source classes, it’s acceptable for the Repository to handle threading.</p><p>❌ <strong>Contain Business Logic</strong><br>Business logic belongs in the Domain Layer (Use Cases)<strong>. </strong>Repositories are responsible for data orchestration, not decision making about how data should be used.</p><p>❌ <strong>Depend on Use Cases</strong><br>This creates a circular dependency between the Data and Domain layers. Instead, let the Use Case gather all the required inputs before calling the Repository.</p><p>❌ <strong>Depend on Other Repositories</strong><br>Repositories should not depend on other Repositories. Each Repository is responsible for handling a specific Data Source or data type. Introducing cross-repository dependencies leads to tight coupling. If you need data from multiple sources, aggregate it in a Use Case in the Domain Layer.</p><h4>3. Caching in Repositories</h4><p>Repositories are a good place to trigger<strong> </strong>caching, as long as no business logic needs to be applied before the data is stored. If a REST API response is directly cache-able, the Repository should <strong>store it immediately after retrieval</strong>.</p><pre>internal class NavigationRepositoryImpl @Inject constructor(<br>    private val navigationRemoteDataSource: NavigationRemoteDataSource, // interface<br>    private val navigationLocalDataSource: NavigationLocalDataSource, // interface<br>) : NavigationRepository {<br><br>    override suspend fun getNavigationApi(): Result&lt;Unit&gt; {<br>        return when (val result = navigationRemoteDataSource.getNavigationApi()) {<br>            is Result.Success -&gt; insertNavigationToCache(result.data)<br>            is Result.Error -&gt; Result.Error(result.error)<br>        }<br>    }<br><br>    private suspend fun insertNavigationToCache(navigationModel: NavigationModel): Result&lt;Unit&gt; {<br>        return when (val result = navigationLocalDataSource.insert(navigationModel) {<br>            is Result.Success -&gt; Result.Success(Unit)<br>            is Result.Error -&gt; Result.Error(result.error)<br>        }<br>    }<br>    <br>}</pre><p>In this example, the Repository delegates both the remote fetch and the cache insert to their respective Data Sources. It doesn’t handle <em>how</em> those operations are done, only <em>when</em> and <em>in what order</em> they occur.</p><h4>Processing Data Before Caching</h4><p>If the data needs to be transformed, validated, or amended before being cached. For example, if business logic determines which parts of the data to store or if it must be combined with another source, then that work should happen in the<strong> </strong>Domain Layer, inside a Use Case.</p><p>Let the Use Case perform the necessary transformations, then pass the final, domain result back to the Repository to be persisted. This keeps business logic out of the Data Layer and ensures a separation of concerns.</p><h4>Caching Strategies</h4><p>Caching is a key part of the Data Layer, but there’s no one-size-fits-all approach. Your strategy should depend on how fresh the data needs to be. For example, static content like app settings benefits from caching, while real time data (e.g. shopping carts, stock levels) should not be cached to avoid serving outdated information. 🔗 <a href="https://developer.android.com/topic/architecture/data-layer/offline-first">See the official Android Developers documentation</a> for a comprehensive look at caching strategies in Android.</p><h4>4. Multiple Repositories &amp; Dependency Aggregation</h4><p>Repositories should not contain business logic or inter-repository communication. Allowing Repositories to directly communicate with each other can lead to a tight coupling. If you need to aggregate data from multiple Repositories utilise a Use Case.</p><p><strong>Why Use Cases Aggregate Data<br></strong>When a screen or feature needs data from more than one source, the Use Case knows how to coordinate those calls, apply any business logic, and return the result in a way that makes sense to the Presentation Layer. This keeps the architecture modular and easier to maintain as the application grows.</p><p>❌ <strong>Incorrect Example: Aggregating Data in a Repository</strong></p><pre>internal class StockRepositoryImpl @Inject constructor(<br>    private val pricingRepository: PricingRepository, // ❌ Cross-repository dependency<br>    private val stockRemoteDataSource: StockRemoteDataSource<br>) : StockRepository {<br><br>    override suspend fun getStockSummary(productId: String): StockSummary {<br>        val price = pricingRepository.getPriceForProduct(productId) // ❌ Unrelated to stock<br>        val stockLevel = stockRemoteDataSource.getStockLevel(productId)<br>        return StockSummary(productId, stockLevel, price)<br>    }<br>}</pre><h4><strong>Why is this incorrect?</strong></h4><p>❌ <strong>Violates the Single Responsibility Principle (SRP)<br></strong>The Repository is now responsible for fetching stock <strong>and</strong> price data, which introduces unnecessary coupling.</p><p>❌ <strong>Tightly Couples Repositories<br></strong>If PricingRepository changes, StockRepository might need modifications, even though it’s unrelated to pricing data.</p><p>❌ <strong>Increases Test Complexity<br></strong>Unit tests for StockRepository now need to mock or fake the PricingRepository, making testing more cumbersome.</p><p>✅ <strong>Correct Example : Aggregate Data in the Use Case<br></strong>The correct approach is to let Repositories focus on their own data concerns and move any coordination or transformation logic into a Use Case.</p><pre>internal class GetStockSummaryUseCase @Inject constructor(<br>    private val pricingRepository: PricingRepository, // interface<br>    private val stockRepository: StockRepository // interface<br>) {<br>    suspend operator fun invoke(productId: String): StockSummary {<br>        val price = pricingRepository.getPriceForProduct(productId)<br>        val stockLevel = stockRepository.getStockLevel(productId)<br>        return StockSummary(productId, stockLevel, price)<br>        // ✅ Keeps responsibilities clearly separated<br>    }<br>}</pre><h4><strong>Why is this correct?</strong></h4><p>✅ <strong>Repositories stay independent<br></strong>Each repository does one thing (stock or pricing), and doesn’t depend on the other.</p><p>✅ <strong>The Use Case Handles Coordination<br></strong>The GetStockSummaryUseCase orchestrates the data from both Repositories, applies any business rules (if needed), and returns the final result to the <strong>Presentation Layer</strong>.</p><p>✅ <strong>Easier to Test and Maintain<br></strong>Repositories are easier to test in isolation, and changes in pricing logic won’t impact the StockRepository.</p><blockquote>🔗 <a href="https://medium.com/asos-techblog/android-clean-architecture-understanding-the-domain-layer-28bf81d83ef7">Checkout the previous article in the series</a> for a deep dive on the Domain Layer and Use Cases.</blockquote><h3>Understanding Data Sources</h3><h4>1. What is a Data Source?</h4><p>A Data Source is responsible for accessing and managing data from a specific location, whether that’s over the network, on local storage, or from an in-memory cache. It abstracts the details of how that data is retrieved or persisted, offering a focused interface for performing CRUD operations.</p><p>Data Sources also handle data transformation, mapping raw responses (such as DTOs or database entities) into domain friendly models before passing them downstream.</p><blockquote>💡 If your architecture doesn’t use explicit Data Source classes, then mapping and transformation work will usually shift into the Repository. While that can work for smaller apps, having separate Data Source classes offers clearer boundaries and better scalability.</blockquote><h4>2. Types of Data Sources</h4><p>There are typically two main types of Data Sources</p><p><strong>Remote Data Sources</strong><br>Handle communication with remote APIs. They’re responsible for sending requests, parsing responses, and mapping API results into domain models.</p><p><strong>Local Data Sources</strong><br>Manage access to on-device storage, such as databases, file systems, or preference stores. These sources handle local persistence, and typically use tools like Room or DataStore.</p><h4>3. How to Structure Data Sources</h4><p>Each Data Source should focus on a single data entity. For example, a UserRemoteDataSourceImpl would work with a UserDto, while a UserLocalDataSourceImpl would handle a UserEntity from Room.</p><p>While it’s ideal to separate by storage type (e.g. Room vs. DataStore), it’s also perfectly valid to combine them behind a single local data source interface, as long as the responsibilities stay cohesive and the Repository isn’t exposed to the underlying storage details.</p><h4>4. Naming Data Sources</h4><p>Data Source naming should follow the format:</p><blockquote><strong><em>[Type of Data]</em></strong> + <em>[</em>type of source<em>]</em> + <em>DataSource</em></blockquote><p>Examples:<br>✅ <strong><em>Bag</em></strong><em>Remote</em>DataSource<br>✅ <strong>Checkout</strong><em>Local</em>DataSource</p><blockquote><em>🚫 </em><strong><em>Avoid implementation specific interface names</em></strong><em> <br>(e.g. </em><em>BagSharedPreferencesDataSource). </em>Repositories should not be aware of the underlying storage mechanism, allowing for flexibility in future changes.</blockquote><h3>Best Practices for Implementing Data Sources</h3><h4>1. Separation of Concerns</h4><p>A Data Source should never be accessed directly by other layers of your application, such as Use Cases or ViewModels. Instead, all data flows through a Repository, which serves as a bridge between the Data Sources and the Domain Layer.</p><p>Keeping Data Sources behind interfaces gives you the flexibility to swap out storage mechanisms or REST APIs without impacting the rest of the application. For example, switching from Firebase to a REST API or from Room to DataStore should require no changes outside of the Data Source Implementation class. It also facilitates easier testing by allowing us to swap out the real implementations for test fakes.</p><h4>2. Mapping</h4><p>Mapping between DTOs, Entities, and Domain Models is necessary for maintaining separation of concerns and avoiding tight coupling between layers.</p><p><strong>Where Should Mapping Happen?<br></strong>Mapping should ideally occur inside the Data Source classes. This ensures that by the time data reaches the Repository, it’s already in Domain Model form. This keeps Repositories focused purely on orchestration.</p><p>If your project doesn’t have explicit Data Source classes, it’s acceptable for the Repository to take on this responsibility. If your Repositories become bloated and complex, consider moving the mapping logic to the Data Source.</p><p><strong>How Should You Map?<br></strong>There are two standard approaches to mapping:</p><ul><li>Use a dedicated mapper class</li><li>Use a Kotlin extension function</li></ul><p><strong>Which One Should You Choose?</strong></p><ul><li>If the mapper has dependencies use a stand alone mapper class.</li><li>If the mapper has no dependencies use an Kotlin extension function.</li></ul><p><strong>Mapping Extension Functions<br></strong>When using extension functions for mapping, place them in a clearly named Kotlin file such as UserMappingExtensionFunctions.kt, located in the /data/mapper package. This file contains mapping functions between a UserDto, UserModel &amp; UserEntity. These functions must be <strong>unit tested</strong>, just like standalone mappers. Create a corresponding test class alongside them.</p><pre>// Example of a Kotlin extension mapping function<br>fun UserDto.toDomain() = User(<br>    id = this.id,<br>    name = this.name,<br>    email = this.email<br>)</pre><pre>// Example of a stand alone mapper class<br>class UserDtoToDomainMapper @Inject constructor() {<br>    fun map(dto: UserDto): User {<br>        return User(<br>            id = dto.id,<br>            name = dto.name,<br>            email = dto.email<br>        )<br>    }<br>}</pre><blockquote>💡 Avoid using default values to mask missing data (e.g. empty strings or zero values) unless you’re intentionally handling optional fields. Failing silently can lead to hard-to-trace bugs in the UI or Domain Layer.</blockquote><h4>3. Error Handling &amp; Loggin<strong>g</strong></h4><p>Data Sources are the right place to catch and log exceptions. However, they should never expose raw exceptions to the Repository or Domain Layer. Instead, they should map those exceptions to domain friendly error types.</p><p>This allows the Repository to pass structured, meaningful error information to the Presentation Layer, which can then respond in ways that improve the user experience. For example, showing an error message, disabling a button, or triggering a retry.</p><p>The key principle here is that raw exceptions like HTTP error codes should never be exposed directly to the Domain or Presentation Layers. Instead, exceptions should be mapped to specific error types that make sense in the domain of the application.</p><p><strong>Using a Custom Result Class<br></strong>We use a custom sealed interface called BagResult to represent the outcome of an operation. This helps to unify the way we handle both success and failure.</p><pre>// Example of a custom Result class<br>sealed interface BagResult {<br>    data class Success(val data: Bag) : BagResult<br>    data class Error(val error: BagError) : BagResult<br>}</pre><ul><li><strong>Success</strong>: Contains the data returned from the operation (from a REST API call or database query).</li><li><strong>Error</strong>: Contains a BagError, which is a domain friendly class containing predefined errors that could happen when interaction with the Bag API.</li></ul><p>The BagResult class allows us to handle both the success and failure of an operation in a consistent manner, simplifying error propagation through different layers of the app.</p><p><strong>Mapping to Domain Specific Error Types<br></strong>To keep error handling at the data layer clear and domain focused, we use a <strong>enum class</strong> to define error types. Here’s an example of how we define a custom error class for handling different types of errors related to a shopping bag REST API.</p><pre>enum class BagError {<br>    NetworkError,<br>    InvalidRequest,<br>    Unauthorized,<br>    Forbidden,<br>    NotFound,<br>    InternalServerError,<br>    UndefinedError<br>    // additional cases can be added...<br>}</pre><p>The BagError enum captures specific error conditions that might occur in the context of fetching or managing bag data. The benefit of using such an approach is that we can clearly differentiate between various types of errors (e.g. network issues, invalid requests, or server errors). This makes it easier to respond to errors appropriately in the UI, like showing relevant error messages, redirecting the user to the login screen etc.</p><p>In this case, we use a BagErrorMapper.kt with a top level function to map raw exceptions to domain specific error type:</p><pre>internal fun mapToBagError(exception: Throwable): BagError {<br>    return when (exception) {<br>        is IOException -&gt; BagError.NetworkError<br>        is HttpException -&gt; mapHttpErrorCode(exception)<br>        else -&gt; BagError.UndefinedError<br>    }<br>    <br>    private fun mapHttpErrorCode(exception: HttpException): BagError {<br>        return when (exception.code()) {<br>            400 -&gt; BagError.InvalidRequest<br>            401 -&gt; BagError.Unauthorized<br>            403 -&gt; BagError.Forbidden<br>            404 -&gt; BagError.NotFound<br>            in 500..599 -&gt; BagError.InternalServerError<br>            else -&gt; BagError.UndefinedError<br>        }<br>    }<br>}</pre><p>By handling errors at the Data Source level, we:<br>✅ Ensure consistency by logging errors at the point of failure.<br>✅ Provide meaningful errors to the Domain &amp; Presentation layers.<br>✅ Predictable error types facilitates easier testing.<br>✅ Avoid redundant logging across multiple Repositories.<br>✅ Keep Repositories focused on orchestration.</p><h4>CancellationException Handling</h4><blockquote>⚠️ <strong>Important</strong>: If using runCatching, ensure that you explicitly <strong>rethrow CancellationExceptions</strong>. If a Coroutine is cancelled due to a timeout or job cancellation, it should propagate downstream instead of being swallowed. Failing to do so can lead to unexpected behavior such as infinite loading states or unresponsive UIs.</blockquote><p>We use a helper function to automatically re-throw the CancellationException.</p><pre>suspend fun &lt;R&gt; runSuspendCatching(block: suspend () -&gt; R): Result&lt;R&gt; {<br>    return try {<br>        Result.success(block())<br>    } catch(c: CancellationException) {<br>        throw c<br>    } catch (e: Throwable) {<br>        Result.failure(e)<br>    }<br>}</pre><h4>4. Dependency Injection</h4><p>Use standard constructor injection for any Data Source that’s required immediately. If the Data Source is optional or rarely used (a cache that’s only hit in certain flows), you can use lazy injection. This defers instantiation until it’s actually needed, which helps reduce memory usage and improve app startup time.</p><h4>5. Threading</h4><p>In Coroutines, libraries like Retrofit, Ktor, Room &amp; DataStore internally use Dispatchers.IO for network/disk operations so it’s not necessary to manually switch threads. However, if you’re reading/writing from local storage files or any other custom storage solution you will need to manually switch to a background thread using Dispatcher.IO in the DataSouce class before the operation occurs.</p><h3>Implementing Remote Data Sources</h3><p>Remote Data Sources handle communication with remote sources, most commonly REST APIs. Libraries like Retrofit or Ktor are commonly used to handle network requests. Each Remote Data Source should define an interface that specifies how it interacts with the REST API.</p><h4>1. Naming REST API Interfaces</h4><p>REST API interfaces should follow the format:</p><blockquote><strong><em>[Feature] </em></strong><em>+ Api</em></blockquote><p>Examples:<br>✅ <strong>Bag</strong>Api<br>✅ <strong>Checkout</strong>Api</p><pre>interface BagApi {<br>    @GET(&quot;bags/{bagId}&quot;)<br>    suspend fun getBagByBagId(<br>        @Path(&quot;bagId&quot;) bagId: String,<br>    ): BagDto<br>}</pre><h4><strong>2.</strong> ❌ <strong>Incorrect Example of a Remote Data Source</strong></h4><p>Here is a Remote Data Source implementation that violates Clean Architecture boundaries and introduces unnecessary complexity.</p><pre>internal class BagRemoteDataSourceImpl @Inject constructor(<br>    private val bagApi: BagApi, // interface<br>    private val bagDtoToDomainMapper: BagDtoToDomainMapper,<br>    private val userRepository: UserRepository, // ❌ Not allowed<br>    @IODispatcher private val ioDispatcher: CoroutineDispatcher // ❌ Unnecessary<br>) : BagRemoteDataSource {<br><br>    override suspend fun getBag(): BagResult&lt;Bag&gt; {<br>      return withContext(ioDispatcher) { // ❌ Retrofit handles threading internally<br>          runCatching { // ❌ Use runSuspendCatching to auto rethrow CancellationExceptions<br>            bagApi.getBag(userRepository.getUserId()) // ❌ User id should be passed in getBag()<br>          }.fold(<br>              onSuccess = { result -&gt;<br>                  BagResult.Success(bagDtoToDomainMapper.map(result)) // ❌ Any malfomed json errors may crash the app<br>              }<br>          },<br>              onFailure = { exception -&gt;<br>                  val bagError = mapToBagError(exception) <br>                  // ❌ No error logging to crash reporting tools<br>                  BagResult.Error(bagError) <br>              }<br>           )<br>        }<br>    }<br>}</pre><h4>Why is this incorrect?</h4><p>❌ <strong>Circular Dependency<br></strong>The Data Source is depending on a Repository (UserRepository). This breaks the unidirectional data flow and creates a circular dependency. Values like the user id should be retrieved by the Domain Layer and passed as parameters.</p><p>❌ <strong>Unnecessary Thread Management<br></strong>An explicit CoroutineDispatcher is injected and used via withContext. This is unnecessary when using Retrofit with Coroutines, as Retrofit already handles network calls off the main thread.</p><p>❌ <strong>No Error Logging</strong><br>It’s important that we log errors using crash reporting tools so we can monitor REST API’s in production.</p><p>❌ <strong>Mapping Errors Not Caught</strong><br>Any NullPointerExceptions thrown during mapping due to malformed JSON will crash the app as the mapping occurs outside of the runCatching. An example of this could be an issue with the contract not being honoured and returning different data than expected.</p><p>❌ <strong>Using runCatching</strong><br>It’s preferable to use the runSuspendCatching helper function mentioned above as this will re-throw CancellationExceptions for you, negating the need for developers to remember to do this manually.</p><h4>3. ✅ Correct Example of a Remote Data Source</h4><p>The following version respects the principles of Clean Architecture and avoids anti-patterns.</p><pre>internal class BagRemoteDataSourceImpl @Inject constructor(<br>    private val bagApi: BagApi, // interface<br>    private val crashLogger: CrashLogger, // interface<br>    private val eventLogger: EventLogger, // interface<br>    private val bagDtoToDomainMapper: BagDtoToDomainMapper<br>) : BagRemoteDataSource {<br><br>    override suspend fun getBag(userId: String): BagResult&lt;Bag&gt; { // ✅ User id passed as a param<br>      return runSuspendCatching { // ✅ runSuspendCatching re-throws CancellationExceptions<br>          val result = bagApi.getBag(userId) // ✅ No thread switching<br>          bagDtoToDomainMapper.map(result) // ✅ Map in the runSuspendCatching to catch malformed json<br>        }.fold(<br>            onSuccess = { mappedResult -&gt;<br>                BagResult.Success(mappedResult)<br>            }<br>        },<br>            onFailure = { exception -&gt;<br>                val bagError = mapToBagError(exception) // ✅ Use mapper function to convert raw throwable to domain error type<br>                logError(exception, bagError) // ✅ Log exceptions<br>                BagResult.Error(bagError) <br>            }<br>        )<br>    }<br><br>    private fun logError(<br>        exception: Throwable, <br>        bagError: BagError<br>    ) {<br>        Log.e(this.javaClass.simpleName, exception.message.toString())<br>        crashLogger.logException(exception)<br>        <br>        eventLogger.logEvent(<br>            eventAttributes = mapOf(<br>                NAME to bagError.name,<br>                DESCRIPTION to exception.message.orEmpty()<br>            )<br>        )<br>    }<br>}</pre><h4>Why is this correct?</h4><p>✅ <strong>No Circular Dependencies<br></strong> The user id is passed as a parameter rather than fetched from a Repository which violates Clean Architecture principles. This keeps the Data Source free of business logic and external dependencies.</p><p>✅ <strong>Threading Delegation</strong><br>Threading is delegated to and managed internally by Retrofit, removing the need to inject a dispatcher or switch threads manually. This simplifies the setup and testing.</p><p>✅ <strong>Mapping</strong><br>The DTO is converted to a domain model before the result is returned to the Repository.</p><p>✅ <strong>Error Logging</strong><br>Errors are logged at the source and mapped to domain specific types allowing monitoring in production. Logging tools are abstracted through decoupled interfaces. This makes the implementation easy to test and replace if logging tools change in the future.</p><p>✅ <strong>Uses runSuspendCatching</strong><br>Using runSuspendCatching re-throws CancellationExceptions for us automatically and preserves structured concurrency.</p><h3>Implementing Local Data Sources</h3><p>Local Data Sources are responsible for interacting with data stored on the device. They follow the same Clean Architectural structure as Remote Data Sources but instead interact with local storage mechanisms such as databases, files, or key-value stores.</p><h4>1. Storage Options</h4><p>The choice of local storage depends on the type of data and how it will be accessed:</p><ul><li><strong>Room / SQLDelight<br></strong> Use a database when your data is structured, relational, and requires long-term persistence. Databases allow complex queries like joins, filters, and transactions.</li><li><strong>DataStore <br></strong> DataStore is the modern, Coroutine backed replacement for SharedPreferences. It is best suited for small sets of data like user settings, flags, or preferences that do not require complex querying.</li><li><strong>Files (JSON, CSV, XML, etc)<br></strong> Files are suitable for configuration settings, logs, or exportable data that don’t require querying.</li><li><strong>In-memory Cache<br></strong> This is temporary storage for frequently accessed data objects that do not expect to persist if the app is killed or restarted.</li></ul><blockquote>⚠️ Be cautious with in-memory caches. If the app is placed in the background and a process death occurs, the system may kill the process to reclaim resources. When the user returns to the app, it will attempt to restore the previous state, but the in-memory cache will have been cleared. This can result in unintended UI states &amp; crashes.</blockquote><h4>2. Naming DAO Interfaces</h4><p>When using a database like Room, a DAO is where you define how the app interacts with the local tables (inserts, queries, deletes, etc).</p><p>DAO interface naming should follow the format:</p><blockquote><strong><em>[Type of Data] </em></strong><em>+ Dao</em></blockquote><p>Examples:<br>✅ <strong>Navigation</strong>Dao<br>✅ <strong>SearchHistory</strong>Dao</p><pre>@Dao<br>interface NavigationDao {<br><br>    @Query(&quot;SELECT * FROM navigation WHERE id = :id&quot;)<br>    suspend fun getNavigation(id: String): NavigationEntity?<br><br>    @Insert(onConflict = OnConflictStrategy.REPLACE) // replaces duplicate entries<br>    suspend fun insertNavigation(navigation: NavigationEntity)<br><br>    @Delete<br>    suspend fun deleteNavigation(navigation: NavigationEntity)<br>}</pre><blockquote>💡 DAO methods using suspend functions automatically run on a background thread thanks to Room’s Coroutine support, so you don’t need to explicitly manage threading.</blockquote><h4>3. Local Data Source Example</h4><p>Local and Remote Data Sources share a similar structure: dependency injection, error handling, result wrapping, and mapping to domain models. For these shared patterns, refer to the Remote Data Source section above. The key difference is that instead of injecting a REST API interface, you inject the DAO interface when using Room, or an appropriate interface when working with DataStore or other local storage mechanisms.</p><h3>Understanding Data Layer Models</h3><p>In the Data Layer, two primary types of models are used:</p><ul><li><strong>DTOs </strong>represent REST API response models. They should remain immutable and map closely to the REST API response.</li><li><strong>Entities</strong> represent database tables and persist data within the local database. They are typically annotated with @Entity when using Room. 🔗 <a href="https://developer.android.com/training/data-storage/room/defining-data">See the official Room documentation</a> for more details on structuring Entities.</li></ul><h4>1. Naming Data Models</h4><p>Data models should follow the format:</p><blockquote><strong><em>[Type of Data] </em></strong><em>+ </em>Dto/Entity</blockquote><p>For example:<br>✅ <strong>Navigation</strong>Dto<br>✅ <strong>Navigation</strong>Entity</p><h4>2. Best Practices for DTOs</h4><p>✅ <strong>Immutable Data Classes</strong><br>Make DTOs immutable by using val properties only. This helps avoid accidental changes and keeps your data consistent.</p><p>✅ <strong>Nullability Handling</strong><br>Match the nullability of DTO properties to the REST API specification. Handle any nulls during mapping to avoid crashes later in the app.</p><p>✅ <strong>Mappers for Transformation</strong><br>Use a dedicated mapper to transform DTOs to Entities or Domain Models. This ensures that no unnecessary REST API details leak into the other layers.</p><p>✅ <strong>Serialization &amp; Field Mapping</strong><br>If your JSON field names differ from your Kotlin property names, use the @SerialName annotation from Kotlin Serialization. Kotlin Serialization is preferred over Gson due to its better performance and native Kotlin support.</p><blockquote>⚠️ Keep in mind that @SerialName helps prevent property obfuscation when using tools like R8.</blockquote><p>✅ <strong>Avoid API-Specific Fields<br></strong>Exclude any API metadata fields such as status codes or timestamps that aren’t needed. Keep DTOs focused only on the data required.</p><p>✅ <strong>Separation of Concerns</strong><br>DTOs should focus only on<strong> </strong>data representation. Avoid embedding business logic or including mapping functions within DTO classes.</p><pre>import kotlinx.serialization.SerialName<br>import kotlinx.serialization.Serializable<br><br>@Serializable<br>data class NavigationDto(<br>    @SerialName(&quot;navigation_id&quot;) <br>    val id: String?,<br>    @SerialName(&quot;navigation_title&quot;) <br>    val title: String?,<br>    @SerialName(&quot;navigation_url&quot;) <br>    val url: String?<br>)</pre><h3>Package Structure</h3><p>Below is an example of how the Data Layer can be structured. This is a simple representation and could change in a modularised app.</p><pre>📂 data<br> ├── 📂 local<br> │    ├── BagDao.kt<br> │    ├── BagEntity.kt<br> │    ├── BagEntityMapper.kt<br> │    ├── BagLocalDataSourceImpl.kt<br> │<br> ├── 📂 remote<br> │    ├── BagApi.kt<br> │    ├── BagDto.kt<br> │    ├── BagErrorMapper.kt<br> │    ├── BagDtoMapper.kt<br> │    ├── BagRemoteDataSourceImpl.kt<br> │<br> ├── 📂 repository<br> │    ├── BagLocalDataSource.kt  // interface<br> │    ├── BagRemoteDataSource.kt. // interface<br> │    ├── BagRepositoryImpl.kt<br><br>📂 domain<br> ├── 📂 repository<br> │    ├── BagRepository.kt // interface</pre><h3>Conclusion</h3><p>A well-structured Data Layer built with Clean Architecture does more than organise your code. It sets the stage for scalability, testability, and long-term maintainability. By separating data management from business and presentation logic through Data Sources and Repositories, you create an architecture that’s easier to evolve. With solid practices like dependency injection, proper mapping, threading, and error handling, your Data Layer becomes a strong foundation your app can confidently grow on.</p><p><em>I’m Sean Coyle and I joined the ASOS Android team in August 2022. I started my career in Android development in 2019. Before that, I was a social worker and spent a few years teaching English in Japan.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a8df03b508c2" width="1" height="1" alt=""><hr><p><a href="https://medium.com/asos-techblog/android-clean-architecture-understanding-the-data-layer-a8df03b508c2">Android Clean Architecture: Understanding the Data Layer</a> was originally published in <a href="https://medium.com/asos-techblog">ASOS Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Introducing Test-Driven Vibe Development]]></title>
            <link>https://medium.com/asos-techblog/introducing-test-driven-vibe-development-0effe6430691?source=rss----6757df096022---4</link>
            <guid isPermaLink="false">https://medium.com/p/0effe6430691</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[qa]]></category>
            <category><![CDATA[vibe-coding]]></category>
            <dc:creator><![CDATA[Irfan M]]></dc:creator>
            <pubDate>Wed, 15 Oct 2025 10:38:39 GMT</pubDate>
            <atom:updated>2025-10-22T15:01:39.791Z</atom:updated>
            <content:encoded><![CDATA[<p><em>Note: The draft was submitted on the 8th of August. Some of the processes and challenges mentioned in this article have been refined since. We’ll be sharing these updates soon.</em></p><p>In the coding sphere, technology is sprinting at a breakneck pace, with new LLM-driven capabilities debuting nearly every day. When it comes to software development, the nirvana — “software developing software” — has shifted from fantasy to a realistic proposition. We are in the eye of a genuine paradigm shift in how we deliver products.</p><p>But revolutionary changes are rarely clean. And this one is not without its challenges and a sea of sceptical voices questioning its viability.</p><p>In this piece, I’ll share how we’ve experimented with adapting this incredible, if disruptive, technology into our software development lifecycle in a way that limits some of the common pitfalls accompanying agentic LLMs in real-world enterprise-level product builds. Along the way, I’ll recount a few humbling lessons and observations from our journey close to the edge of this exciting frontier.</p><h3>Vibing Coding</h3><p>In the beginning of 2025, AI researcher Andrej Karpathy coined the term vibe coding to describe the process where, “developers primarily use natural language prompts — speaking or typing in plain language — to instruct AI tools to generate, refine, and debug code.” In this approach, users focus on what they want (the “vibe”) while letting the AI handle the code implementation.<a href="#_ftn1">[1]</a></p><p>Below is a diagram illustrating the vibe coding cycle:</p><figure><img alt="A circular flowchart illustrating the AI-driven code generation process. It includes six connected nodes: ‘Natural Language Input,’ ‘AI Interpretation,’ ‘Code Generation,’ ‘Execution and Observation,’ ‘Feedback and Refinement,’ and ‘Repetition Execution-Feedback,’ forming a continuous loop." src="https://cdn-images-1.medium.com/max/1024/1*fu4C49MJQr4WmxR5ID1TJg.png" /></figure><p>There are challenges with vibe coding. These include managing the quality of code due to hallucinations, missing defects buried deep in the code which can become a costly fix later on, or the agentic tool developing insecure code.</p><h3>LLM limitations</h3><p>When I first tried using an AI agent to develop code, my experience matched these widely documented concerns. Plenty of vibe coding energy but not much rigour. But coming from a quality engineering background, I could see that this was fundamentally a quality problem, be it in process, prompting, or AI output.</p><p>Whenever I approach a new project, I start with the solution architecture to gain a better grasp of what I’m dealing with. And so here too, I turned to the LLM’s own architecture.</p><p>LLMs are built on the transformer model. While the responses of LLMs are often mind-bendingly impressive, at their core, these systems are fundamentally probabilistic pattern-recognition engines. At the basic level, they ingest words (tokens) and predict the next token by assigning probabilities to a sequence of words to generate the output.<a href="#_ftn2">[2]</a></p><p>A major challenge with this architecture is attention management: how the model decides which words in the input deserve focus when working out what words to use in the generated output. The probability function used in this process (softmax)<a href="#_ftn3">[3]</a> is intrinsically unable to handle large complex sentences as the “attention” becomes too thinly spread.<a href="#_ftn4">[4]</a> This means that accuracy declines as the context increases and the attention diffuses.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/612/1*9zN67ZtuXK1BJ8n6E3n_1g.png" /><figcaption>An example of how an agent managing context: summarising the conversation for later reference.</figcaption></figure><p>Agents have strategies to mitigate this limitation, but the takeaway was clear. To help the AI agent maintain its accuracy, it would make sense to employ fewer, more atomic tasks for a single instruction prompt.</p><p>Given this insight, I felt a part of the solution wasn’t to shoehorn the AI agent into the existing software development lifecycle or remain bound by vibe coding, but to rethink the lifecycle in a way that factored the LLM’s inherent constraints.<a href="#_ftn5">[5]</a></p><h3>Test-Driven Vibe Development (TDVD)</h3><p>The concerns with vibe coding can be addressed by fusing the mature test-first quality engineering practices, which we have already established here at ASOS, with vibe coding. The result is Test-Driven Vibe Development (TDVD): a restructuring of the classic Vibe Coding process that integrates the discipline of established test-first quality engineering practices with the useful aspects of the vibe coding AI paradigm:</p><p>· Architecture driven testing to form a highly relevant test strategy.<a href="#_ftn6">[6]</a></p><p>· ATDD to establish quality-refined acceptance criteria/test scenarios.</p><p>· BDD for consistent domain language expression.</p><p>· TDD to front load the tests in the implementation phase.</p><p>In TDVD, natural language prompts drive the definition of the expected behaviour of the solution upfront through acceptance criteria or tests. This test-first, intent-driven approach ensures that:</p><p>· Clear testable requirements as code are generated before functional implementation.</p><p>· Code generation is aligned with testable requirements.</p><p>· AI-generated implementations are continuously validated against those requirements.</p><p>· Bugs and hallucinations are caught early, before they become costly.</p><p>· Security and quality risks are managed proactively.</p><p>In short, TDVD inverts the current (vibe coding) development lifecycle approach, which sees the functional code generated first before testing occurs. It creates a flow from intent → specification → test → code → validation → deployment.</p><p>The activities within the TDVD flow fall within three broad categories.</p><p>· Plan — defining the intent in a test-first manner</p><p>· Implement — developing the intent in a test first manner.</p><p>· Harden — final layer of deepening functional and non-functional test depth.</p><figure><img alt="Flowchart titled ‘Test-Driven Vibe Development’ with a central cycle of Plan, Implement, and Harden. Plan includes idea refinement, solution design, threat modelling, PRD, and test strategy. Implement includes step generation and is supported by a right-side loop: solution propagation, context setting, and prompt implementation. Harden includes test and resilience hardening, supported by a left-side loop: review, feedback, and refactor." src="https://cdn-images-1.medium.com/max/1024/1*4K0tDSb0a4eC_TPrvM0vqw.png" /></figure><h3>The Trial</h3><p>From May through to June, I fleshed out details of a potential TDVD lifecycle and built out supporting prompts. With the framework in place, the next step was finding a a low-risk candidate for a real-world test.</p><p>The chosen candidate was an internal reporting solution that calculated stock discrepancies based on stock movement transactions. Although it could indicate a misalignment, it was unable to show what was missing or why, resulting in time-consuming manual investigations and delays in resolving physical versus system stock discrepancies.</p><p>What was needed was a solution that could precisely identify and report missing or mismatched stock transactions by comparing actual physical stock position in our warehouse management systems and stock reconciliation records in an automated manner. The solution would enable earlier discrepancy detection, proactive alerting, reduced reliance on manual investigations, faster resolution of stock mismatches, better accountability, and improved stock accuracy for downstream consumers.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/904/1*yuJaxUQwP4Rg8tSCOwu4Lg.png" /><figcaption>Screen capture of the legacy Summary report page.</figcaption></figure><p>The team had several constraints: an engineering lead already juggling multiple commitments, a single part-time senior developer, no frontend expertise, and no dedicated product manager.</p><p>Still, I kicked off the trial with the brilliantly forward-thinking Engineering Lead, Caroline Perryman, and the ever-adaptive Senior Developer Cian Moloney. Together we set out to push the limits of what was possible with this vibe-infused, quality first lifecycle model in an environment that was anything but conducive.<a href="#_ftn7">[7]</a></p><p><strong>Findings</strong></p><p>The MVP we set out to build consisted of the following:</p><p>· A backend comprising an API, Azure Functions for data handling, timed triggers, and discrepancy calculation logic, and two data stores.</p><p>· A React-based frontend report with three views, each supporting search and dynamic filtering capabilities, plus a notes modal</p><p>· Standard essentials: authorisation, authentication, and logging.</p><p>Initial estimates for a team of developers to build an MVP was <strong>between 4–6 months</strong> — 3–4 months for the backend and 2–3 for the frontend.</p><p>With Caroline, Cian (at 50% availability), and myself, we delivered the MVP plus some nice-to-have capabilities (an additional File Detail View, which provided a detailed drill-down view showing comprehensive file-level metadata and SKU-level discrepancy data, and a report export feature) in <strong>4 weeks</strong>.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/904/1*ItmKijKlr44Msx5CgNuvtA.png" /><figcaption>Screen capture of the new reporting service: Dashboard view.</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/903/1*j2N6Ti8vOaSHhW_Dk4HXJQ.png" /><figcaption>Screen capture of the new reporting service: Warehouses view.</figcaption></figure><p>During this period, we rebuilt approximately 80% of the MVP twice to refine our prompts so that they achieved higher-quality outputs.</p><p>If we exclude the two tear downs and combine the Planning and the actual Implement phases, the total time active development time was 42 hours or 11 workdays.<a href="#_ftn8">[8]</a></p><p>The results are positive. <strong>We were 7–10 times faster </strong>than the original MVP estimate and we also delivered extra capabilities, all with a team effectively consisting of two and a half people.</p><p>Of course, it must be stressed that the original estimate was based on the team’s lack of experience in the technology stack. Another team might have quoted something leaner. Even so, within our context and constraints, the acceleration was undeniable.</p><h3>Lessons</h3><p>TDVD combines some of the fluidity, speed, and creativity of Vibe Coding with the discipline of a test-first blueprint, and guardrails attuned to the LLM’s architecture. Our experiment proved the concept has legs, but the journey wasn’t exactly smooth sailing.</p><p>We tore down the solution and rebuilt it twice even though we got it working each time. Why? Our features were not small enough and, consequently, not iterative enough. We failed to maintain the LLM’s context between lengthy prompt phases. We adapted by revising our PRD by breaking the capability features down into much smaller user stories, and rebuilding our implementation step guides so that each cycle focused on no more than two features. We also introduced implementation summaries using our own custom prompt to provide continuity for subsequent implementation phases.</p><p>Other niggles included the agent building out tests and the associated code, but not executing the tests, which resulted in mounting technical debt. At other times, the agent confidently claimed it had completed a revision or fix but left failing tests unresolved. The solution was what any good TDDer would do in this situation. We prompted the AI to never proceed to the next task step until all tests passed.</p><p>On the specificity of instructions, we discovered a curious trade-off. Slightly more vague instructions gave the AI room for creative assumptions. For example, when asked to “add a page,” it not only built the page but updated the navigation bar too. Overly precise instructions, on the other hand, killed that initiative. When we sharpened the prompt and told the agent exactly how to add the page, it did so, but didn’t update the navigation bar.</p><p>Finally, it pays (premium request!) dividends to really think about the dependencies. Before starting the Implement phase of TDVD, it’s worth ensuring the skeleton solution includes all the prerequisites and installations already. Failing to consider this or failing to guide the agent on handling missing dependencies will result in token-draining technical debt and rework.</p><h3>Final thoughts and next steps</h3><p>Perhaps the most radical of all observations weren’t about the AI. They concerned us humans in the experiment.</p><p>First, the obstacles to delivery. The biggest time sink wasn’t necessarily the code generation. It was the infrastructure plumbing. Chasing down people for access and configuration cost us several days of development time. This is an area which could do with increased automation and perhaps some “agentic” attention to accelerate delivery further.</p><p>Second, rapid skill acquisition and augmentation. We started as backend specialists and walked out having built a full stack solution with technologies we had never touched before the trial.</p><p>Third, roles in the TDVD lifecycle. I wanted to push the boundaries of the experiment and from the outset stipulated that we were not assuming fixed roles. The results were both surreal and empowering at the same time:</p><p>· Caroline shifted between product manager, systems specialist, solutions architect, and a tech lead.</p><p>· Cian assumed the roles of a solution architect and platform engineer, while vibe coding and testing chunks of both the backend and frontend solution.</p><p>· I designed, orchestrated, and continually revised the TDVD development process, helped Caroline define the product, designed the solution, and coded and tested portions of the full stack implementation.</p><p>We mobbed, we swapped roles, and we blurred boundaries beyond our specialisms. The dynamism in the teamwork was on a higher plane, an organic outcome of the TDVD flow.</p><p>So, what does this all mean? Well, it’s an exciting time to be involved in software engineering, for sure. But more significantly, early signs suggest TDVD might just be the recipe for stable, quality-infused, yet rapid product delivery.</p><p>Or, to put it less formally: the vibe is strong with this one.</p><p><strong>Citations:</strong></p><p><a href="#_ftnref1">[1]</a> Google. “Vibe Coding Explained: Tools and Guides.” <em>Google Cloud</em>, 2025, cloud.google.com/discover/what-is-vibe-coding.</p><p><a href="#_ftnref2">[2]</a> For a useful and easy to digest overview, see: Google. “Introduction to Large Language Models | Machine Learning.” <em>Google for Developers</em>, 8 Aug. 2023, developers.google.com/machine-learning/resources/intro-llms.</p><p><a href="#_ftnref3">[3]</a> <a href="https://en.wikipedia.org/wiki/Softmax_function">https://en.wikipedia.org/wiki/Softmax_function</a></p><p><a href="#_ftnref4">[4]</a> Veličković, Petar, et al. “Softmax Is Not Enough (for Sharp Out-of-Distribution).” <em>ArXiv (Cornell University)</em>, 1 Oct. 2024, <a href="https://doi.org/10.48550/arxiv.2410.01104.">https://doi.org/10.48550/arxiv.2410.01104.</a></p><p><a href="#_ftnref5">[5]</a> There is some academic support for pre-emptive measures through workflow optimisation to assist agentic responses. The researchers in one study examining why agents struggle to resolve real-world GitHub issues stated the following:</p><p>“Improving the workflow of software development agents is a promising direction. Enhancing an agent’s process with early, proactive measures that pre-empt common yet pervasive errors may reduce overhead and improve resolution rates.” — Chen, Z., Ma, W. &amp; Jiang, L. (2025) ‘Unveiling Pitfalls: Understanding Why AI-driven Code Agents Fail at GitHub Issue Resolution’, arXiv preprint arXiv:2503.12374. Available at: <a href="https://arxiv.org/abs/2503.12374">https://arxiv.org/abs/2503.12374</a>.</p><p><a href="#_ftnref6">[6]</a> ADT is a model I developed for forming test strategies directly from architecture diagrams.</p><p><a href="#_ftnref7">[7]</a> I’m grateful to Caroline and Cian for believing in the idea, backing the trial, and showing such patience and commitment in bringing it to life.</p><p><a href="#_ftnref8">[8]</a> We recorded 32 hours mob sessions and approximately 10 hours of individual work.</p><p><em>Irfan is a Principal Engineer specialising agentic delivery at ASOS. He is passionate about leveraging AI beyond AI-assisted coding to deliver quality products at pace. Currently proving a reimagined, real-word, quality-first, AI-driven product development lifecycle.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0effe6430691" width="1" height="1" alt=""><hr><p><a href="https://medium.com/asos-techblog/introducing-test-driven-vibe-development-0effe6430691">Introducing Test-Driven Vibe Development</a> was originally published in <a href="https://medium.com/asos-techblog">ASOS Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Benchmarking Nixtla x Microsoft’s TimeGEN-1: A Real-World Demand Forecasting Comparison]]></title>
            <link>https://medium.com/asos-techblog/benchmarking-nixtla-x-microsofts-timegen-1-a-real-world-demand-forecasting-comparison-d01e92a8ae51?source=rss----6757df096022---4</link>
            <guid isPermaLink="false">https://medium.com/p/d01e92a8ae51</guid>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[forecasting]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Anita Clement]]></dc:creator>
            <pubDate>Thu, 24 Apr 2025 09:09:53 GMT</pubDate>
            <atom:updated>2025-08-06T13:04:44.758Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*yoW-xpWgfSsu5cVG_hvRnA.png" /><figcaption>Nixtla partners with Microsoft to integrate time-series forecasting into Azure, <a href="https://www.nixtla.io/news/timegen1-on-azure">https://www.nixtla.io/news/timegen1-on-azure</a></figcaption></figure><p>​​​​</p><h3><strong>Demand Forecasting at ASOS</strong></h3><p>With a catalogue of nearly 50,000 products at any given time, our AI-based demand forecasting system is critical for maintaining optimal inventory levels. We leverage machine learning to help our merchandising teams replenish the right number of products at the right time. Our aim is to accurately predict future demand for each product to avoid stock-outs while minimising excess inventory in our warehouses.</p><p>While our current demand forecast achieves good accuracy and operates at scale, we are always on the lookout for new technologies that could further improve our system’s performance. Last year, for example, we explored various deep learning frameworks, including Transformers. This year, we decided to dive into TimeGEN-1 and will be sharing in this post its capabilities, how to use it, and how it performed on our benchmark.</p><h3><strong>What are TimeGPT and TimeGEN-1?</strong></h3><p>TimeGPT represents the first attempt at developing a foundational model for time series forecasting. Research in this domain has been very active since then, with the publication of new models such as TimesFM, Chronos, Moirai, Time-MoE or Lag-Llama.</p><p>The model aims to leverage the success of transformer-based architectures in NLP and apply them to time series data. Both text and time series are sequential in nature. Just as transformers excel at processing sequences of words and identifying key words in a sentence, they could similarly focus on time points that influence future values using the self-attention mechanism. TimeGPT’s architecture was specifically designed for handling time series data though, providing the flexibility to process time series with varying frequencies and characteristics, as well as different input sizes and forecasting horizons.</p><p>TimeGPT was trained on over 100 billion data points, sourced from a diverse collection of publicly available time series across various domains such as finance, healthcare, and sales. The assumption is that, having been exposed to such a rich dataset, the model should be able to generalise and accurately forecast new scenarios, with fine-tuning or even in a zero-shot context. This is analogous to ChatGPT, which has been trained on extensive text data, enabling it to understand and generate human-like text across various topics and contexts.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/507/1*YQeocQX2722u2einF7iOwQ.png" /><figcaption>Illustration of TimeGPT’s training and its ability to forecast on unseen data from <a href="https://arxiv.org/pdf/2310.03589">TimeGPT-1</a></figcaption></figure><p>TimeGPT was more recently added to Azure AI’s Model as a Service (MaaS) offering as a result of a partnership between Microsoft and Nixtla, under the name TimeGEN-1.</p><h3><strong>Forecasting with TimeGEN-1</strong></h3><p>We have decided to apply TimeGEN-1 to an operational demand forecasting problem at ASOS. Our goal is to forecast demand at a very granular level, specifically at a product level and by warehouse on a weekly basis, for thousands of products. Additionally, we need to forecast for the next 52 weeks to accommodate the longer lead times of certain products. Lead times refer to the period between placing an order and receiving the product in our warehouses, which can vary significantly depending on the supplier and product type.</p><h4><strong><em>Setting Up TimeGEN-1 in Azure AI</em></strong></h4><p>The first step to forecasting with TimeGEN-1 is to deploy a TimeGEN-1 model with Azure AI Foundry. Microsoft’s documentation on how to <a href="https://learn.microsoft.com/en-us/azure/ai-studio/how-to/deploy-models-timegen-1">create a new deployment</a> is straightforward. Once deployed, the only thing you will need is the Target URI and the Key value.</p><h4><strong><em>Forecasting with TimeGEN-1</em></strong></h4><p><strong>Instantiating the Client</strong></p><p>The only package you need to install is Nixtla’s SDK. You will then instantiate the client with the Target URI and the Key value mentioned just above.</p><pre>from nixtla import NixtlaClient <br> <br>nixtla_client = NixtlaClient( <br> base_url=&quot;your_target_uri&quot;, <br> api_key=&quot;your_api_key&quot;, <br> timeout=None, <br>)</pre><p>The timeout argument corresponds to the request timeout in seconds. The default value is 60 but in our case we had to set it to None due to the size of our payload.</p><p><strong>Processing the Data</strong></p><p>The processing required to call the forecasting endpoint is the same as for the rest of the time series ecosystem developed by Nixtla (MLForecast , NeuralForecast , etc.). You will need a unique ID column, a timestamp column, and provide the future values of your exogenous variables (here future_exog_df).</p><p><strong>Calling the Forecasting Endpoint</strong></p><p>Once the data is processed, calling the forecasting endpoint becomes very straightforward. You just need to pass the dataframe containing historical data (both the target and the exogenous variables), the dataframe containing the future values of the exogenous variables, the number of horizons you want to forecast for, and the name of the ID, time, and target columns. Since we are using the Azure AI endpoint, the model has to be set to ‘azureai’.</p><p>The Nixtla endpoint provides users with two options in terms of models: timegpt-1 and timegpt-1-long-horizon. Nixtla recommends using the latter if you want to predict more than one seasonal period, which is our case here. Unfortunately it is not available as part of the Azure AI API at the time of writing.</p><p>Note that the API does not come with a cross-validation feature so you will have to implement your own.</p><pre>nixtla_client.forecast( <br> df=df, <br> X_df=future_exog_df, <br> h=HORIZON, <br> freq=&#39;W-MON&#39;, <br> id_col=&#39;unique_id&#39;, <br> time_col=&#39;ds&#39;, <br> target_col=&#39;target&#39;, <br> model=&#39;azureai&#39;, <br>)</pre><h4><strong><em>Evaluation</em></strong></h4><p>The performance of TimeGEN-1 against our baseline model, a tree-based model, was assessed using a 4-fold rolling window cross-validation framework spanning over a year to account for variations in accuracy due to seasonality. In rolling window cross-validation, a fixed-size training window is moved across the data, and the model is trained on each window and evaluated on the subsequent window.</p><p>We also included a naïve forecast in our benchmark as an additional point of comparison. A naïve forecast predicts future values by simply using the most recent observed value as the forecast for all future periods.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*L-5JQ14WoM-k0Ckwv5Cn4w.png" /><figcaption>Metric comparison between TimeGEN-1 and a naïve forecast, and TimeGEN-1 and our baseline.</figcaption></figure><p>TimeGEN-1 is less accurate than our baseline, but its performance remains impressive, especially considering it operates in a zero-shot setting. We also tested fine-tuning, but this feature is limited to one parameter with TimeGEN-1 (whereas TimeGPT allows fine-tuning up to five parameters), resulting in worse accuracy than zero-shot in our case.</p><p>To diagnose things further, we look at the bias metric and its breakdown by quantile. Bias is calculated as the difference between the prediction and the actual value. The overall bias of TimeGEN-1 is -2.3 units, suggesting that this model tends to underpredict for our use case.</p><p>Ideally, we would get a model where the bias is roughly the same across quantiles. In the graph below, we compared the average prediction to the average target by quantile. The target distribution was divided into 10 quantiles, but the first five appear as one because their average targets are very close.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*QrLYp-Nu5ZR0-PMhO7RoRw.png" /><figcaption>Calibration plot of TimeGEN-1, 10 bins</figcaption></figure><p>In an ideal scenario, the red line, representing the average predictions from TimeGEN-1 by target quantile would be as close as possible to the dotted line, the identity line, which represents perfect calibration. However, both lines start diverging significantly from the 7th quantile, indicating that the model suffers from a lack of calibration.</p><p>In the context of demand forecasting, lack of calibration, and significant under-prediction of high sellers specifically is a big issue. High sellers, i.e., the top quantile, are products that show very high sales volumes compared to the average and represent a significant proportion of overall sales. Under-predicting would lead to stockouts and missed opportunities. This is why at ASOS we have focused on calibrating our models and wouldn’t be able to productionise TimeGEN-1 as it currently stands.</p><h4><strong><em>A note on cost</em></strong></h4><p>At the time of writing, the input cost was $0.00013 per 1,000 tokens, and the output cost was $0.00038 per 1,000 tokens. One token corresponds to one data point in the input or output dataset. The total input cost can therefore be summarised by the formula below:</p><p><em>Total input cost = $0.00013 x number of rows x (number of columns — 2*) / 1000</em></p><p>.</p><p>Overall, the input cost will increase with the amount of historical data being used, as well as the number of unique IDs you want to forecast for (e.g., products) and the number of exogenous variables being used. A similar equation can be derived for the total output cost.</p><p>*The unique ID and timestamp columns don’t count.</p><h3><strong>An Opinion on TimeGEN-1’s Capabilities</strong></h3><p>TimeGEN-1 is a very flexible framework. It supports the use of exogenous variables, unlike Time-MoE or Chronos. This capability is fundamental for providing additional context, which helps the model make more accurate predictions. It can also generate probabilistic forecasts and prediction intervals using the robust conformal prediction framework. This enables better risk management and more informed decision-making, particularly in the context of inventory decisions and managing safety stocks to limit stockouts.</p><p>The second version of the TimeGPT API has introduced SHAP values for model interpretability, although they are not yet implemented in Azure. Similarly, Nixtla has made two versions of their model available: a generic one and one for long-horizon forecasting. Unfortunately, no equivalent of the latter is available in Azure AI, which represents a real limitation for some use cases.</p><p>A significant downside is the persistence of fine-tuned models, which is currently not readily available. In a production environment with daily inference, this would necessitate fine-tuning TimeGEN-1 each day. This approach is unnecessary, as the model doesn’t need to be retrained or fine-tuned every single day, and it also has cost implications.</p><h3><strong>Conclusion</strong></h3><p>Nixtla’s collaboration with Microsoft to release TimeGEN-1 was a great initiative. It made Nixtla’s foundational model more widely accessible and streamlined integration with the rest of the ecosystem for Azure users.</p><p>The zero-shot accuracy of this model, although still a bit far from our baseline, remains quite impressive. It will be interesting to see how close we get once fine-tuning is fully made available, as well as the long-horizon model.</p><p>In terms of development, the team at Nixtla has been focusing on the right features, from explainability with SHAP to probabilistic forecasts and uncertainty quantification using conformal prediction, through to hierarchical forecasting. However, TimeGEN-1 is several versions behind TimeGPT-1, resulting in a more limited experience.</p><p>Additionally, until the persistence of fine-tuned models is enabled, it will not be a production-ready, enterprise-grade solution.</p><h3><strong>References</strong></h3><ul><li>Azul Garcia, Cristian Challu and Max Mergenthaler-Canseco. “TimeGPT-1”. May 2024. <a href="https://doi.org/10.48550/arXiv.2310.03589">https://doi.org/10.48550/arXiv.2310.03589</a>.</li><li>How to deploy a TimeGEN-1 model with Azure AI Foundry, <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/deploy-models-timegen-1">https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/deploy-models-timegen-1</a></li></ul><p><em>This article was written by Anita Clement, lead of the AI Forecasting team at ASOS.com. In her spare time, she enjoys playing the piano and cooking for others. Thank you to Tom Evans and Eduardo Nigri for reviewing this blog.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d01e92a8ae51" width="1" height="1" alt=""><hr><p><a href="https://medium.com/asos-techblog/benchmarking-nixtla-x-microsofts-timegen-1-a-real-world-demand-forecasting-comparison-d01e92a8ae51">Benchmarking Nixtla x Microsoft’s TimeGEN-1: A Real-World Demand Forecasting Comparison</a> was originally published in <a href="https://medium.com/asos-techblog">ASOS Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How we revamped the ASOS QA engineering competency]]></title>
            <link>https://medium.com/asos-techblog/how-we-revamped-the-asos-qa-engineering-competency-8255246ae9c1?source=rss----6757df096022---4</link>
            <guid isPermaLink="false">https://medium.com/p/8255246ae9c1</guid>
            <category><![CDATA[learning]]></category>
            <category><![CDATA[engineering]]></category>
            <category><![CDATA[product-management]]></category>
            <category><![CDATA[quality-assurance]]></category>
            <category><![CDATA[career-development]]></category>
            <dc:creator><![CDATA[Irfan M]]></dc:creator>
            <pubDate>Fri, 28 Mar 2025 12:23:06 GMT</pubDate>
            <atom:updated>2025-10-02T08:38:30.053Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="An image of a summary of QA Engineer competencies" src="https://cdn-images-1.medium.com/max/1024/1*1qQQojrhivBDl-poezuvVg.png" /><figcaption>A Summary View of a filled in QAE Competency</figcaption></figure><p>They say <em>if it ain’t broke, don’t fix it</em>. But as QA Engineers (QAE) at heart, our job is to break things intentionally, all in the name of<em> improving </em>the end product, of course.</p><p>Faced with a role restructure and a company-wide roll out of the product operating model (POM), we took our critical eye to the QA engineering competency and sought to rethink the way we structured the key skills to better fit the new world.</p><p>So, armed with the existing competency, our decades worth of experience, and a stubborn drive to keep quality engineering at the cutting edge, we set out to rebuild the matrix from the ground up.</p><p>Here’s how we did it: refining the theory, debugging the old, refactoring the structure, and optimising the new so that our QA engineers have a clear reference point for the new era.</p><p><strong>Legacy competency</strong></p><p>The now legacy competency was a solid and proven foundation to work with, reflecting cutting-edge practices that our QAEs were already engaged in.</p><p>The legacy competency was underpinned by three fundamental quality principles.</p><p>1. Test first</p><p>2. Test automation first</p><p>3. Continuous quality</p><p>These principles informed our legacy competency matrix by emphasising rigorous analysis through test first methodologies. In this regard, we have pioneered Architecture-Driven Testing, which provides a methodical model to scrutinising product designs, and made extensive use of the analytical components of Acceptance Test Driven Development and Behaviour Driven Development. The competency skills also encouraged quality practices that facilitated small, high-frequency, high-speed releases.</p><p>As such, the legacy competency was already equipped to adapt to the “build” dimension of the POM, which stipulates small frequent releases as a principle.</p><p>With these foundations, we set about moulding the new competency to accommodate several considerations.</p><p><strong>The Product Operating Model</strong></p><p>POM centres the notions of “product” and outcomes. This means every problem we tackle should have a clear problem statement, and every solution should be judged by measurable success (i.e., not just a gut feeling).</p><p>We wanted these two facets to be more than just theory. That meant two things. First, that a product-first, outcome-driven mindset were woven into QAE practices. Second, structuring the themes of our competency framework to mirror how QAEs engage throughout the product development lifecycle.</p><p>With that in mind, we designed the following structure:</p><figure><img alt="Image of a taxonomy of QA Engineer skill groupings organised to reflect the product lifecycle including managing risk, understanding the product, product development, product release, and community of practice." src="https://cdn-images-1.medium.com/max/904/1*IY0sfMtw5NI3jxIpiy8wdQ.png" /><figcaption>“Productified” taxonomy of QAE skill groupings organised to reflect the product lifecycle.</figcaption></figure><p>With this new structure we introduced the following to better reflect POM:</p><p>1. Language: We deliberately anchored our terminology in the notion of the “product”. For example, instead of using terms like “team test strategy” or “system test strategy”, we shifted to “product test strategy” for skills related to its formation. While this may seem trivial, we felt it was crucial in aiding the fostering of a product mindset, since language can impact how we see things.<a href="#_ftn1">[1]</a></p><p>2. Identifying and measuring KPIs: KPI’s are essential for determining whether the problem we have set out to achieve has been solved. From a quality perspective, they act as <em>meta-level acceptance criteria</em>, going beyond the standard behavioural acceptance criteria QA engineers often scrutinise, build out, and refine. As such, we included skills to both ensure KPIs were considered in the “Understanding the Product” (analysis) and “Product Release” phases (instrumentation and measurement), while informing the “Product Development” phase.</p><p>3. Forming KPIs: Within the “Product Development” phase, we wanted QAEs to think about the technology enablers that drive product success. Thus, we introduced skills around the formation of quality engineering KPIs, which directly feed back into the Product Discovery phase.</p><p>Why does this matter? Consider the scenario where product problem statements have <em>stacked implications</em>. Suppose a product team wants to launch features experimentally but struggles with agility because they keep breaking production. That’s not just an engineering problem — it’s a <em>quality</em> problem. To direct a solutionising lens and effort, QAEs should drive KPI formation around the change failure rate. By tracking and improving this metric, they can identify quality gaps and push for more mature quality and deployment practices in their teams. In short, KPIs not only measure success, they help steer the quality strategy.</p><p>4. Product Risk: We subsumed the notion of product risk (particularly “feasibility”, which relates more to engineering) into the topic of “managing risk”. The topic of managing risk impacts the whole product development lifecycle.</p><p><strong>Reflecting the QA Learning Programme</strong></p><p>At ASOS, we have developed a bespoke and comprehensive QA learning programme (QALP), which reflects our ASOS engineering practices.</p><p>The programme has three levels, and each level contains multiple modules:</p><p>· Foundation — lays down the foundational principles and functional and non-functional quality engineering practices.</p><p>· Intermediate — expands the test-first principle by proposing a quality model that scrutinises architecture, ensuring contextualised, technically-informed product test strategies.</p><p>· Advanced — a new approach and model to product quality, which anchors quality and risk in reliability concerns.</p><p>Although the legacy competency factored the practices we were advancing in our QALP, we wanted to align the training to the competency even more coherently.</p><p>To achieve this, we updated both the skills and role definitions at each level to reflect the QALP’s maturity levels. This ensures that as QAEs progress, their growth is directly supported by the evolving best practices we champion, making the competency framework not just a reference, but a true compass for skills development.</p><p><strong>Levels: continuous cycle of growth</strong></p><p>The adaptation of the QALP to role expectations leads us nicely into the final set of changes: the introduction of new role levels.</p><p>A separate blog will help explain the rationale and changes made for the new structure.</p><p>Here, we will elaborate on how we tailored the new role levels into the QA engineering competency.</p><p>ASOS introduced the following changes to the career ladder:</p><p>· A new associate level role (level 2)</p><p>· A new mid-level role (level 4)</p><p>· A re-aligned senior role (level 6)</p><p>· A staff level role (level 7)</p><p>The roles distinctions were broadly based on the mastery of skills and the ability to apply them across diverse contexts. In other words, progression was not merely about knowing what to do, but demonstrating expertise in how, when, and where to apply these skills effectively.</p><p>The diagram below provides an illustration of how we distinguished the roles based on mastery:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/904/1*FKShXpMZsSa4pI8Hk2MKwg.png" /></figure><p>Here is how we broke the QAE level down:</p><p>Level 1: QAEs focus on developing their understanding of quality principles and practices.</p><p>Level 2: they are building foundational quality engineering skills and actively applying these the quality lifecycle, gaining hands-on experience in real-world scenarios.</p><p>Level 3: Entering the mid-level engineering space, QAEs should have mastered foundational skills, applying them in a single product context. This marks the beginning of their journey into intermediate-level expertise.</p><p>Level 4: The key shift here is in scope: QAEs expand their experience beyond a single product or service. They also build their intermediate level skills and start exploring advanced quality practices. Senior behaviours also emerge at this stage, including mentoring juniors and supporting the adoption of good quality practices.</p><p>Level 5: QAEs achieve a mastery in advanced quality practices and actively drive mature quality lifecycle practices within their product team.</p><p>Level 6: QAEs evolve into technical leaders who quantifiably enhance quality through advanced functional and non-functional specialist skillsets within a single product team. They have the capability to apply their skills across different technology solutions within their product area.</p><p>Level 7: This role assumes mastery of prior skills but shifts focus to solving complex, strategic quality problems. The role is more strategic, requiring the Staff QAEs to demonstrate impact through metrics aligned to product goals. While they may align to a single team at any given moment, their <em>scope </em>of activities is defined by the problem, which often spans multiple teams.</p><p>In short, the framework is not just a checklist exercise. Its phases reflect a progressive cycle of learning, applying, and diversifying applicative experience.</p><p><strong>Continuous quality</strong></p><p>For us, revamping the QA engineering competency matrix was about redefining how we empower our QA engineers to grow.</p><p>By aligning it with the Product Operating Model (POM) we have built a framework that not only helps transition the company into the POM world but also remains adaptable for the future.</p><p>Moreover, in ensuring the competency framework is directly supported by our QA Learning Programme, QAEs have both the knowledge of the expectations they need to develop and the tools to get there.</p><p>Finally, we leveraged the new role level structure to smooth progression, avoiding skill gaps between jumps.</p><p>Of course, like any good quality engineer, we know this is not a one-and-done effort. Just as we continuously test and refine our products, we’ll keep iterating on the quality competency to ensure it remains valuable and meaningful. Because at ASOS, quality engineering is not afterthought. It’s always improving, always learning, and always striving to push the boundaries of what great quality engineering looks like.</p><p><strong>Citation</strong>:</p><p><a href="#_ftnref1">[1]</a> <a href="https://www.scientificamerican.com/article/our-language-affects-what-we-see/">https://www.scientificamerican.com/article/our-language-affects-what-we-see/</a></p><p><em>Irfan is a Principal Test Engineer at ASOS. He is passionate about making quality engineering the cornerstone of both delivering innovative, quality products and building organisations that enable them.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8255246ae9c1" width="1" height="1" alt=""><hr><p><a href="https://medium.com/asos-techblog/how-we-revamped-the-asos-qa-engineering-competency-8255246ae9c1">How we revamped the ASOS QA engineering competency</a> was originally published in <a href="https://medium.com/asos-techblog">ASOS Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Android Clean Architecture: Understanding the Domain Layer]]></title>
            <link>https://medium.com/asos-techblog/android-clean-architecture-understanding-the-domain-layer-28bf81d83ef7?source=rss----6757df096022---4</link>
            <guid isPermaLink="false">https://medium.com/p/28bf81d83ef7</guid>
            <category><![CDATA[android]]></category>
            <category><![CDATA[clean-architecture]]></category>
            <category><![CDATA[mobile-app-development]]></category>
            <category><![CDATA[apps]]></category>
            <category><![CDATA[android-app-development]]></category>
            <dc:creator><![CDATA[Sean Coyle]]></dc:creator>
            <pubDate>Tue, 11 Mar 2025 13:21:08 GMT</pubDate>
            <atom:updated>2025-11-05T21:07:29.079Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="An image of the Android logo with a brush cleaning — a visual representation of Clean Architecture." src="https://cdn-images-1.medium.com/max/1024/1*nwW7Uhqa3hDq9TlUbPCjnA.jpeg" /><figcaption>Image generated by DALL-E</figcaption></figure><p>As Android apps grow in complexity, an unstructured codebase can lead to tight coupling, duplicated logic, and fragile unit tests which ultimately slows down development. Clean Architecture principles address these challenges by enforcing a clear separation of concerns, making applications more scalable, maintainable, and testable.</p><p>At the heart of this architecture, the <strong>Domain Layer </strong>plays a crucial role in encapsulating business logic, ensuring a distinct boundary between the UI and data handling. This article deep dives into the Domain Layer, explaining its purpose, best practices, and how to implement it effectively in Android applications.</p><h3>What is the Domain Layer?</h3><p>The Domain Layer is responsible for handling the core business logic of an application. It acts as an independent layer, separate from both the Presentation Layer (which manages the UI and user interactions) and the <a href="https://medium.com/asos-techblog/android-clean-architecture-understanding-the-data-layer-a8df03b508c2">Data Layer</a> (which handles data storage and retrieval from sources like databases and REST APIs).</p><p>While the Presentation and Data Layers depend on the Domain Layer, it remains completely decoupled from them. This ensures that business logic remains unaffected by changes in UI frameworks or data sources.</p><figure><img alt="A diagram which depicts the three layers in Clean Architecture. The Presentation Layer, the Domain Layer and the Data Layer." src="https://cdn-images-1.medium.com/max/1024/1*If9QK8tE1VlzSDc_5CEKKw.jpeg" /><figcaption>Dependency Arrangement in Clean Architecture</figcaption></figure><p>The Domain Layer primarily contains:</p><ul><li><strong>Use Cases</strong> <strong>(Interactors): </strong>Handle specific pieces of business logic, defining how data should be processed and used within the app.</li><li><strong>Domain Models: </strong>Represent entities within the business domain.</li><li><strong>Repository Interfaces</strong>: Define how data can be accessed without specifying the underlying source (such as a database or REST API), ensuring decoupling from the internals of the Data Layer.</li></ul><figure><img alt="A diagram showing a representation of data flow through the Clean Architecture layers starting in the Presentation Layer with a Fragment, moving to the ViewModel then crossing into the Domain Layer with a Use Case. The Use Case then calls the Repository Interface. We then cross into the Data Layer into the Repository Implementation. From here we can either go to a Local Data Source or a Remote Data Source to send or fetch information." src="https://cdn-images-1.medium.com/max/1024/1*rYeTfVXyDbwqpVaj9E0EmA.jpeg" /><figcaption>Representation of Data Flow In Android Clean Architecture</figcaption></figure><h3>Key Responsibilities of the Domain Layer</h3><p>✅ <strong>Encapsulates Business Logic and Promotes Modularity</strong><br>The Domain Layer centralizes and encapsulates business logic, ensuring it’s decoupled from both the Presentation Layer and the Data Layer. It defines Use Cases, which represent specific business workflows like placing an order or validating user input. Additionally, by isolating logic in this layer, it can be shared using Kotlin Multiplatform (KMP), reducing logic duplication with iOS and other platforms.</p><p>✅ <strong>Decouples Business Logic from Data Sources</strong><br>The Domain Layer ensures business logic remains independent of data sources by relying on abstractions rather than concrete implementations. This allows flexibility in how data is fetched or persisted, so changes like switching from REST APIs to GraphQL or modifying database providers do not affect core business logic.</p><p>✅ <strong>Improves Testability</strong><br>By keeping business logic in small, single-responsibility classes, the Domain Layer enhances testability. Use Cases depend on abstractions (e.g. Repository interfaces), making it easy to mock dependencies and write unit tests in isolation. This design supports test-driven development (TDD) and ensures high test coverage.</p><p>✅ <strong>Reduces Complexity in the Presentation Layer</strong><br>By handling business logic and transformations, the Domain Layer ensures that ViewModels remain lean, focusing solely on managing UI state. Extracting logic from ViewModels into Use Cases allows us to reuse it across the application. This improves maintainability, making the Presentation Layer easier to test.</p><p>❌ <strong>Not Responsible for Data Storage or Retrieval</strong><br>The Domain Layer does not handle data persistence or retrieval. These responsibilities belong to the <a href="https://medium.com/asos-techblog/android-clean-architecture-understanding-the-data-layer-a8df03b508c2">Data<strong> </strong>Layer</a>, which includes components like Repositories, Data Sources, REST APIs and Databases. The Domain Layer remains agnostic to how data is fetched or persisted.</p><p>❌ <strong>Not Responsible for UI State Management</strong><br>The Domain Layer does not manage UI state, this is handled by the Presentation Layer (ViewModels). Instead, the Domain Layer executes business logic and returns immutable results.</p><h3><strong>The Anatomy of a Use Case</strong></h3><p>A Use<strong> </strong>Case is a class that encapsulates a single business operation, making it reusable and easy to test. A well-structured Use Case should have only <strong>one</strong> public function, as it should focus on a single responsibility.</p><p>If a Use Case has multiple public functions, it may be handling too many concerns, violating the <strong>Single Responsibility Principle (SRP)</strong>. In such cases, consider splitting the logic into separate, more focused Use Cases.</p><h4>1. Naming Use Cases</h4><p>Use Case naming should follow the format:</p><blockquote><strong><em>[Verb</em></strong><em>] </em>+<em> [</em>Noun<em>]</em> + <em>UseCase</em></blockquote><p>Examples:<br>✅ <strong><em>Get</em></strong><em>CustomerBag</em>UseCase<br>✅ <strong><em>Logout</em></strong><em>User</em>UseCase<br>✅ <strong>Cache</strong><em>OrderDetails</em>UseCase</p><h4>2. Defining Business Logic</h4><p>Business logic defines the rules and decisions that control how the app behaves in different situations. It determines:</p><ul><li><strong>How data is processed — </strong>applying transformations before displaying it.</li><li><strong>When and how actions should be performed — </strong>validating user input before saving it.</li><li><strong>How different parts of the app interact — </strong>keeping responsibilities separate.</li></ul><h4>3. Use Case Dependencies</h4><p>A Use Case should only depend on abstractions and pure Kotlin constructs, ensuring platform independence. Below is some guidance on what to include and avoid in Use Cases:</p><p>✅ <strong>Other Use Cases — </strong>For shared business logic (use sparingly to avoid excessive chaining).</p><p>✅ <strong>Pure Kotlin Utility Classes — </strong>For reusable logic like validation rules, number formatting etc.</p><p>✅ <strong>Repository Interfaces — </strong>To interact with the Data Layer without relying on concrete implementations.</p><p>❌ <strong>Android dependencies</strong> — Avoid using Context, LiveData, Application etc. Instead of using LiveData prefer Kotlin<strong> </strong>Flow or suspend functions to ensure the Domain Layer remains decoupled from the Android framework.</p><h4>4. How Use Cases Expose Data</h4><p>Use Cases provide data to the Presentation Layer through different mechanisms, depending on how the operation executes:</p><ul><li><strong>Regular function calls</strong> — For synchronous operations (e.g. in-memory checks, simple calculations).</li><li><strong>Suspend functions</strong> — For asynchronous one-time operations (e.g. network requests, database queries).</li><li><strong>Flow</strong> — For reactive streams that emit updates over time (e.g. observing database changes).</li><li><strong>Observables/Callbacks</strong> — For RxJava-based architectures.</li></ul><p>The choice depends on whether the operation is immediate<strong> </strong>and<strong> </strong>lightweight or asynchronous<strong> </strong>and<strong> </strong>long<strong>-</strong>running.</p><h3>Best Practices in Use Cases</h3><h4>1. The Invoke Operator</h4><p>Since Use<strong> </strong>Cases have a single responsibility, we can use the Kotlin<strong> </strong>invoke<strong> </strong>operator to make them callable like functions, eliminating redundant method calls.</p><pre>class GetCustomerBagUseCase @Inject constructor(<br>    private val userRepository: UserRepository, // interface<br>    private val bagRepository: BagRepository // interface<br>) {<br>    suspend operator fun invoke(): Bag {<br>        val userId = userRepository.getUserId()<br>        return bagRepository.getCustomerBag(userId)<br>    }<br>}</pre><p>This allows the call in the ViewModel to be more concise<strong> </strong>and<strong> </strong>intuitive:</p><pre>val customerBag = getCustomerBagUseCase()</pre><h4>2. Thread Safety</h4><p>Use Cases should be <strong>main-thread safe</strong>, meaning they can be called from the main thread without blocking the UI. Long-running operations like REST API calls or database queries, should be delegated to the Data Layer.</p><p>The Domain Layer should not switch to I/O-bound threads (like Dispatchers.IO). The <a href="https://medium.com/asos-techblog/android-clean-architecture-understanding-the-data-layer-a8df03b508c2">Data Layer</a> is responsible for managing the threading and will switch to a background thread as close to the source of the call as possible. However, for CPU-intensive tasks like sorting and transformations, we can use Dispatchers.Default within the Domain Layer.</p><p>Imagine we need to process a batch of fetched products and apply sorting, formatting their creation dates, and calculating discounts.</p><ul><li><strong>Fetching products</strong> → Handled in the Data Layer (e.g. Repository fetching from REST API or database).</li><li><strong>Sorting &amp; Formatting </strong>→ Business logic that belongs in the Domain Layer.</li></ul><p>Since processing these products is <strong>CPU-intensive</strong>, it should be executed on a background<strong> </strong>thread to prevent UI freezes and ANR<strong> (</strong>Application<strong> </strong>Not<strong> </strong>Responding<strong>) </strong>errors.</p><p><strong>✅ Best Practice: Move Heavy Computation to a Background Thread</strong></p><pre>class FormatProductListUseCase @Inject constructor(<br>    private val productRepository: ProductRepository, // interface<br>    private val formatDateUseCase: FormatDateUseCase,<br>    private val calculateDiscountUseCase: CalculateDiscountUseCase,<br>    @CoroutineDefault private val defaultDispatcher: CoroutineDispatcher<br>) {<br>   suspend operator fun invoke(): List&lt;Product&gt; = withContext(defaultDispatcher) {<br>    productRepository.getProducts()<br>        .sortedByDescending { it.price }<br>        .map { product -&gt;<br>            val formattedDate = formatDateUseCase(product.createdAt)<br>            val discount = calculateDiscountUseCase(product.price)<br>            <br>            product.copy(<br>                createdAt = formattedDate,<br>                discount = discount<br>            )<br>        }<br>}<br><br>data class Product(<br>    val id: String,<br>    val name: String,<br>    val price: Double,<br>    val createdAt: String,<br>    val discount: Double = 0.0<br>)</pre><h4>Why This Works</h4><p>✅ <strong>Delegates Fetching to the Data Layer</strong><br>The responsibility of fetching the products is handled by the Data Layer, keeping business logic focused in the Domain Layer.</p><p>✅ <strong>Uses </strong><strong>Dispatchers.Default for CPU-intensive Tasks</strong><br>Sorting and transformations run on a background thread, improving performance and preventing UI<strong> </strong>freezes.</p><p>✅ <strong>Injects </strong><strong>CoroutineDispatcher for Testability</strong><br>This allows for easy replacement with a test dispatcher during unit testing. This ensures that Coroutines execute in a controlled environment, making tests faster, more predictable, and isolated from real threading concerns.</p><p>✅ <strong>Keeps Business Logic Modular</strong><br>By encapsulating the date formatting and calculating discounts in separate, reusable Use<strong> </strong>Cases, the code remains modular, and easier<strong> </strong>to<strong> </strong>maintain.</p><h4>3. Combining Data from Multiple Repositories</h4><p>While it’s possible to merge data from multiple sources in a ViewModel or Repository, doing so can make these classes complex and difficult to test. Instead, it’s better to create a Use<strong> </strong>Case that aggregates data from different Repositories, keeping business logic separate and reusable.</p><p>✅ <strong>Best Practice: Combining Products With User Bag Count Using Flows</strong></p><pre>class GetProductsWithBagCountUseCase @Inject constructor(<br>    private val productsRepository: ProductsRepository, // interface<br>    private val bagRepository: BagRepository, // interface<br>    private val userRepository: UserRepository // interface<br>) {<br>    operator fun invoke(): Flow&lt;ProductsWithBagCount&gt; {<br>        val userId = userRepository.getUserId()<br>        return combine(<br>            productsRepository.getUserProducts(userId),<br>            bagRepository.getUserBagCount(userId)<br>        ) { products, bagCount -&gt;<br>            ProductsWithBagCount(products, bagCount)<br>        }<br>    }<br>}<br><br>data class ProductsWithBagCount(<br>    val products: List&lt;Products&gt;,<br>    val bagCount: Int<br>)</pre><h4>Why This is Better</h4><p>✅ <strong>Uses </strong><strong>Flow.combine()</strong><br>Aggregates data reactively, ensuring any updates from either Repository propagate automatically. combine() emits the latest values from all combined flows.</p><p>✅ <strong>Avoids Blocking Threads</strong><br>Data fetching is asynchronous via Flows, ensuring non-blocking, efficient execution.</p><p>✅ <strong>Keeps the ViewModel Focused</strong><br>The ViewModel only collects a single source of truth, rather than managing multiple Repository calls, making it simpler and more focused on UI state.</p><p>✅ <strong>Repositories Maintain a Single Responsibility</strong><br>Repositories avoid dependencies on one another, maintaining a clear separation of concerns, which simplifies testing and maintenance.</p><p>✅ <strong>Improves Testability</strong><br>The Use Case is easier to mock in unit tests, eliminating the need to handle multiple StateFlow or Flow instances directly in the ViewModel.</p><h4>4. Data Validation</h4><p>Data validation is a critical aspect of ensuring the integrity and correctness of the data flowing through your application. By handling validation logic within the <strong>Domain Layer</strong>, we achieve:</p><p>✅ <strong>Consistency</strong><br>Ensuring data is validated before being processed or presented.</p><p>✅ <strong>Reusability</strong><br>Avoiding redundant validation logic across different parts of the app.</p><p>✅ <strong>Separation of Concerns</strong><br>Keeping validation independent of the Presentation and Data Layers.</p><p><strong>Input Validation</strong><br>Ensures user inputs meet specific criteria before being processed or stored.</p><ul><li>Checking if an email follows the correct format (e.g. user@example.com).</li><li>Verifying that a password meets minimum length and complexity requirements.</li></ul><p><strong>Business Rule Validation</strong><br>Ensures data complies with domain-specific business rules. Frontend validation can help improve UX by checking business rules before an action is performed, based on received data.</p><ul><li>Checking if a user has sufficient funds before placing an order to avoid unnecessary REST API calls.</li><li>Ensuring a product is in stock before proceeding with a purchase. If this check fails we could disable the “Purchase” button.</li></ul><p><strong>Format Validation</strong><br>Ensures data follows a predefined structure or pattern.</p><ul><li>Validating that a date string follows a certain format like YYYY-MM-DD.</li><li>Ensuring a phone number contains the correct number of digits.</li></ul><p><strong>✅ Best Practice: Data Validation in the Domain Layer<br></strong>Let’s consider an example where we need to validate a user’s email address and password before allowing them to register.</p><pre>class ValidateUserRegistrationUseCase @Inject constructor(<br>    private val emailValidator: EmailValidator,<br>    private val passwordValidator: PasswordValidator<br>) {<br>    suspend operator fun invoke(<br>      email: String, <br>      password: String<br>    ): ValidationResult {<br>        return when {<br>            !emailValidator.isValid(email) -&gt; ValidationResult.InvalidEmail<br>            !passwordValidator.isValid(password) -&gt; ValidationResult.InvalidPassword<br>            else -&gt; ValidationResult.Valid<br>        }<br>    }<br>}<br><br>sealed interface ValidationResult {<br>    data object Valid : ValidationResult<br>    data object InvalidEmail : ValidationResult<br>    data object InvalidPassword : ValidationResult<br>}</pre><h4>Validation Overview</h4><p>✅ <strong>Keep Validation Logic Simple and Focused</strong><br>Each validator should focus on one specific task (e.g. validating email format or password length).</p><p>✅ <strong>Error Handling</strong><br>Use sealed classes or sealed interfaces to represent validation states (valid/invalid), making error handling simpler and more readable.</p><p>✅ <strong>Avoid Duplicating Validation Logic</strong><br>Keep validation logic reusable to avoid duplicating code across different parts of the app.</p><p>✅ <strong>Test Validation Logic Independently</strong><br>Since validation logic is part of business rules, unit tests should focus on ensuring that the validation logic works correctly in various scenarios (e.g. invalid email formats, short passwords, etc.).</p><p>❌ <strong>Hardcoding Error Messages</strong><br>Avoid hardcoding error messages. Instead, define error states to ensure consistency across the app.</p><h3>Managing Use Cases: Injection, Lifecycle, and Execution</h3><p>In a ViewModel-based architecture, Use Cases are injected into the ViewModel and executed as needed. Hilt can automatically provide a Use Case if it has only one implementation. To ensure the Use Case lives as long as the ViewModel, annotate it with <strong>@ViewModelScoped</strong>. This ensures the Use Case instance is <strong>created once</strong> when the ViewModel is initialized and remains the same throughout the ViewModel’s lifecycle, avoiding unnecessary re-creations.</p><pre>@ViewModelScoped<br>class GetCustomerBagUseCase @Inject constructor() {}</pre><p>Some Use Cases may benefit from being <strong>@Singleton </strong>scoped which ensures a single instance exists for the lifecycle of the app. This can be useful for persistent business logic like authentication.</p><p>If a Use Case has multiple implementations or follows an interface-based abstraction, bind the implementation in a @Module and install it in ViewModelComponent.</p><pre>@Module<br>@InstallIn(ViewModelComponent::class)<br>abstract class BagModule {<br>    @Binds<br>    @ViewModelScoped<br>    abstract fun bindGetCustomerBagUseCase(<br>        impl: GetCustomerBagUseCaseImpl<br>    ): GetCustomerBagUseCase<br>}</pre><h4>Injection Methods: Standard vs Lazy</h4><p>There are two primary ways to inject Use Cases:</p><ul><li><strong>Standard injection</strong> for frequently used Use Cases</li><li><strong>Lazy injection</strong> for Use Cases that are only needed conditionally</li></ul><h4>1. Standard Injection (For Frequently Used Use Cases)</h4><p>For Use Cases that are used frequently, we can constructor-inject them directly into the ViewModel. The instance is created once when the ViewModel is initialized and remains active throughout the ViewModel’s lifecycle.</p><pre>@HiltViewModel<br>class BagViewModel @Inject constructor(<br>    private val getCustomerBagUseCase: GetCustomerBagUseCase<br>) : ViewModel() {<br><br>    private val _customerBag = MutableStateFlow&lt;CustomerBag?&gt;(null)<br>    val customerBag: StateFlow&lt;CustomerBag?&gt; = _customerBag.asStateFlow()<br>    <br>    fun getCustomerBag() {<br>        viewModelScope.launch {<br>            _customerBag.update { getCustomerBagUseCase() }<br>        }<br>    }<br>}</pre><h4>2. Lazy Injection (For Selectively Used Use Cases)</h4><p>For Use Cases that are only used in specific scenarios, lazy injection delays instantiation until needed. This reduces unnecessary object creation and avoids injecting dependencies that might not be used.</p><pre>import dagger.Lazy<br><br>@HiltViewModel<br>class BagViewModel @Inject constructor(<br>    private val saveItemToWishListUseCase: Lazy&lt;SaveItemToWishListUseCase&gt;<br>) : ViewModel() {<br><br>    fun saveItemToWishList(item: Item) {<br>        viewModelScope.launch {<br>            saveItemToWishListUseCase.get().invoke(item)<br>        }<br>    }<br>}</pre><p>Here, SaveItemToWishListUseCase is not instantiated until get() is called, reducing memory usage and improving app performance when the Use Case isn’t always needed.</p><h4>Use viewModelScope for Coroutine Management 🚀</h4><p>When executing Use Cases within a ViewModel, always use viewModelScope.launch{}. This ensures:</p><ul><li>Coroutines are automatically cancelled when the ViewModel is destroyed, preventing memory leaks.</li><li>Background tasks terminate properly when the UI is no longer active.</li></ul><p>By default, viewModelScope.launch{} uses the <strong>Main Dispatcher</strong>, making it ideal for UI-related operations.</p><blockquote>Note that <em>@ViewModelScoped</em> controls the lifecycle of the Use Case instance. It ensures a single Use Case instance per ViewModel lifecycle, while <em>viewModelScope.launch{}</em> manages Coroutine execution and cancellation. Whilst named similarly these are separate concerns.</blockquote><h4>Key Takeaways</h4><p>✅ <strong>Use Cases persist with the ViewModel</strong><br>Scoping a Use Case with @ViewModelScoped ensures it lives as long as the ViewModel, avoiding unnecessary re-creations.</p><p>✅ <strong>Leverage </strong><strong>viewModelScope.launch{} in ViewModels</strong><br>Runs on the Main Dispatcher by default making it suitable for UI operations and guarantees automatic Coroutine cancellation when the ViewModel is cleared, preventing memory leaks.</p><p>✅ <strong>Choose the right injection method based on usage</strong><br>Standard injection for frequently used Use Cases and lazy injection for optional Use Cases to defer instantiation and reduce memory overhead.</p><p>❌ <strong>Do not inject Use Cases into the Data Layer</strong><br>Use Cases belong to the Domain Layer and should be invoked from the Presentation Layer or other Use Cases when necessary. They should <strong>not</strong> be injected into the <a href="https://medium.com/asos-techblog/android-clean-architecture-understanding-the-data-layer-a8df03b508c2">Data Layer</a> (e.g. Repositories, Data Sources or Mappers) as this violates Clean Architecture principles by introducing unnecessary dependencies and increasing coupling between layers.</p><h3>Data Access Restrictions in the Domain Layer</h3><p>A common question in Clean Architecture is:</p><blockquote>Should ViewModels and other Presentation Layer components access Repositories directly, or should all data access go through Use<strong> </strong>Cases?</blockquote><p>Restricting data access to the Domain Layer has its advantages and drawbacks:</p><p>✅ <strong>Ensures Consistent Business Logic Execution</strong><br>Use Cases enforce rules such as analytics tracking, validation, and data transformations, ensuring that business logic is consistently applied and preventing inconsistent access to raw data from the Data Layer.</p><p>✅ <strong>Supports Modularisation</strong><br>In <a href="https://medium.com/asos-techblog/slaying-the-monolith-api-implementation-modularisation-pattern-in-android-development-22a07c24e9dd">modularised API/Implementation</a> projects, feature modules expose Use Case interfaces in the :api module. This keeps inter-module dependencies minimal, and simplifies the overall architecture.</p><p>✅ <strong>Simplifies ViewModel Testing</strong><br>Since ViewModels depend only on Use Cases instead of Repositories, testing becomes easier. There’s no need to mock Repositories in ViewModel tests, allowing for more isolated, focused, and maintainable unit tests.</p><p>❌ <strong>Introduces Overhead for Simple Fetches</strong><br>When a Use Case simply wraps a Repository call without adding business logic, it can feel redundant and introduce unnecessary complexity.</p><p>At ASOS, we follow the <a href="https://medium.com/asos-techblog/slaying-the-monolith-api-implementation-modularisation-pattern-in-android-development-22a07c24e9dd"><strong>API/Implementation modularisation</strong></a> strategy, where restricting direct data access to the Domain Layer aligns naturally with our architecture. This ensures consistent data handling, reduces coupling, and simplifies long-term maintainability as the project scales.</p><h3>Package Structure</h3><p>Below is an example of how the Domain Layer can be structured. This is a simple representation and could change in a modularised app.</p><pre>📂 domain<br> ├── 📂 model<br> │    ├── ProductsWithBagCount.kt<br> │    ├── Bag.kt<br> │    ├── Product.kt<br> │<br> ├── 📂 repository<br> │    ├── ProductsRepository.kt  //interface<br> │    ├── BagRepository.kt  //interface<br> │<br> ├── 📂 usecase<br> │    ├── AddItemToBagUseCase.kt<br> │    ├── RemoveItemFromBagUseCase.kt</pre><h3>Conclusion</h3><p>Incorporating the Domain Layer into your Android application offers numerous benefits, including scalability, testability, and maintainability. By isolating business logic within the Domain Layer, you ensure that the ViewModel and other UI components remain lean and focused solely on rendering the UI, without the burden of complex operations or data management. This separation of concerns helps create a robust and easily extensible architecture, especially as the app grows in size and complexity.</p><p>By properly structuring Use<strong> </strong>Cases, handling threading efficiently, and deciding on access restrictions wisely, you can build a maintainable app architecture that stands the test of time.</p><p><em>I’m Sean Coyle and I joined the ASOS Android team in August 2022. I started my career in Android development in 2019. Before that, I was a social worker and spent a few years teaching English in Japan.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=28bf81d83ef7" width="1" height="1" alt=""><hr><p><a href="https://medium.com/asos-techblog/android-clean-architecture-understanding-the-domain-layer-28bf81d83ef7">Android Clean Architecture: Understanding the Domain Layer</a> was originally published in <a href="https://medium.com/asos-techblog">ASOS Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ASOS NI x Cancer Fund For Children]]></title>
            <link>https://medium.com/asos-techblog/asos-ni-x-cancer-fund-for-children-b58f896f966c?source=rss----6757df096022---4</link>
            <guid isPermaLink="false">https://medium.com/p/b58f896f966c</guid>
            <category><![CDATA[volunteer]]></category>
            <category><![CDATA[fundraising]]></category>
            <category><![CDATA[volunteering]]></category>
            <category><![CDATA[charity]]></category>
            <dc:creator><![CDATA[Niamh McGuigan]]></dc:creator>
            <pubDate>Wed, 28 Aug 2024 15:08:54 GMT</pubDate>
            <atom:updated>2025-10-02T08:41:59.186Z</atom:updated>
            <content:encoded><![CDATA[<p>ASOS NI started off our year partnership with Cancer Fund For Children in September 2023 after ranking highest voted local charity in our site-wide poll. In the Belfast office, we support two overall charities for the year; The ASOS Foundation (made up of multiple charities including; Centre point, Caramel Rock, OnSide and the Princes Trust) and Cancer Fund For Children, rotating fundraising on a monthly basis.</p><p>Cancer fund for children is an exceptional charity which offers an escape from the hardships faced by families dealing with Cancer, they provide necessary services and support to families during this difficult time.</p><p>We’ve arranged several activities over the year to fundraise and support our chosen charity, and this blog will highlight what we’ve achieved and organised throughout the year.</p><p>We kicked off our fundraising with a short talk from Cancer Fund For Children around what services and support they’ve provided families along with a testimonial from one of the families that benefited from their services. With this new motivation, we then kicked of our charity row. This all took place during ASOS Develops, a two day event which we are entitled to use through ASOS for our own personal development.</p><h3>The Great ASOS Row</h3><p>The row consisted off 13 individuals rowing the distance of 109 km which is the distance of our ASOS Belfast Office to Daisy Lodge and back. Daisy Lodge is a purpose-built therapeutic centre located in Newcastle Co. Down which Cancer Fund For Children offer for families across the island of Ireland affected by cancer.</p><figure><img alt="Image of a team of ASOSers posing by a rowing machine in the gym" src="https://cdn-images-1.medium.com/max/1024/1*y2T7nGWZl0y7JPB5r_bH0g.jpeg" /><figcaption>ASOSers taking part in the row in the Custom House gym</figcaption></figure><p>The row itself kicked off around 8am and finished up at 5pm, rotating consecutively through each of our rowers to reach our 109km target. We divided it up into 15–45 minute time slots. Those that completed 45 minutes enjoyed a bit of competition on the distance which they could cover during that time and there were a few sore legs in the days following. A huge thanks to Custom House Offices for allowing us to utilise the gym for this event.</p><h3>Santa Hats &amp; Raffle</h3><p>Around Christmas time, our charity committee got creative and designed personalised Santa hats. Individuals donated £10 to Cancer Fund For Children, we then designed and created a tailored Santa hat using their name and had them delivered to the office in time for Christmas. We also ran a small raffle for an extra fundraising boost.</p><figure><img alt="ASOSer Molly wearing her ‘Have a Holly Molly Christmas’ hat" src="https://cdn-images-1.medium.com/max/1024/1*xXVmtkH591fI59jgFI52YQ.jpeg" /><figcaption>An ASOSer Molly wearing her ‘Have a Holly Molly Christmas’ hat</figcaption></figure><h3>The Great ASOS Bake Off</h3><p>In February, we held our Great ASOS Bake Off where we had some prizes contributed by a few fantastic local businesses. On the day, everyone in the office could place their votes for first, second and third place with each place being worth 3, 2 or 1 points. To increase the odds of winning even further there where also winners for the most <strong>Aesthetic</strong> and most <strong>Creative</strong> bake.</p><p>Individuals interested in taste testing and voting for each of the baked goods then donated towards our Just Giving page. It was a dough-lightful day for everyone involved, especially those with a sweet tooth!</p><figure><img alt="Image of a tower of cupcakes" src="https://cdn-images-1.medium.com/max/1024/1*clQCeQ9UwthI2k6SkNmW3g.jpeg" /><figcaption>A few of the beautiful bakes from the day</figcaption></figure><h3>Bucket Collections</h3><p>We then offered our volunteering time back in May, collecting donations at the entrance to Belsonic for a few hours with the high footfall traffic. Once finished, we then had time to relax and enjoy the final act. One of the many advantages of partnering with Cancer Fund For Children.</p><figure><img alt="Image of a bucket and poster with a white and yellow Cancer Fund For Children logo" src="https://cdn-images-1.medium.com/max/1024/1*PjyNhKKO--5mrXEJL5rbqA.jpeg" /></figure><h3>Volunteering Day</h3><p>Finally, we visited Daisy Lodge in Newcastle on Monday 29th July to volunteer with the Cancer Fund For Children. We were able to help out with painting and weeding, bringing the lodge back up to peak condition. Before kicking off our tasks, we received a tour of the beautiful retreat they have created and the wonderful restful opportunities they provide to families affected by cancer, both children and parents, and also for bereaved families who have lost someone to cancer over the last 10–15 years.</p><p>We are all inspired by the facilities and services that Cancer Fund For Children provide. Unfortunately Cancer Fund For Children are limited on the number of people they can offer these to at one given time as they currently can only have 200 families on the waitlist and there are hundreds, if not sadly thousands, of families in both Northern Ireland and Ireland in need of their special support.</p><figure><img alt="Image of a group of ASOSers stood outside the front of Daisy Lodge" src="https://cdn-images-1.medium.com/max/1024/1*CG62Jgl-6jBGPB5IF2FqhA.jpeg" /><figcaption>ASOSers who volunteered at Daisy Lodge</figcaption></figure><figure><img alt="Image of toiletries that were donated to Daisy Lodge including shampoo, conditioner, and moisturiser" src="https://cdn-images-1.medium.com/max/1024/1*bNhSYpae-BdddQC8-8BY1A.jpeg" /><figcaption>Products supplied by our ASOS Face and Body Team for Daisy Lodge</figcaption></figure><p>We would love to help Cancer Fund For Children continue to support these families by exceeding our fundraising target of £2,000 before our next financial year when our local charity renews.</p><p>If you feel like you can, please donate using the link below. It’ll be greatly appreciated.</p><p><a href="https://www.justgiving.com/page/asos-ni-1694766038512">Cancer Fund for Children fundraising</a></p><h3>Thank you!!</h3><p><em>Hi, I’m Niamh McGuigan, an Engineering Manager on the Web team at ASOS. I support Web Engineers and QAs in their career growth while ensuring our technology platforms deliver high-quality solutions.</em></p><p><em>I’m also active in the NI Charity Committee and volunteer externally with Women Techmakers. Outside of work, I love planning and going on outdoor adventures — like this one from my recent trip to the Dolomites in Italy!</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kZ51VCIL_qnf9Jtglh0dnA.jpeg" /></figure><p>Feel free to connect with me on Linkedin below ⬇️</p><p><a href="https://www.linkedin.com/in/niamh-mcguigan-6b766aa5/">https://www.linkedin.com/in/niamh-mcguigan-6b766aa5/</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=b58f896f966c" width="1" height="1" alt=""><hr><p><a href="https://medium.com/asos-techblog/asos-ni-x-cancer-fund-for-children-b58f896f966c">ASOS NI x Cancer Fund For Children</a> was originally published in <a href="https://medium.com/asos-techblog">ASOS Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Flow, value, culture, delivery — measuring agility at ASOS (part 2)]]></title>
            <link>https://medium.com/asos-techblog/flow-value-culture-delivery-measuring-agility-at-asos-part-2-c38bfa29ef5b?source=rss----6757df096022---4</link>
            <guid isPermaLink="false">https://medium.com/p/c38bfa29ef5b</guid>
            <category><![CDATA[agile]]></category>
            <category><![CDATA[culture]]></category>
            <category><![CDATA[agility]]></category>
            <category><![CDATA[coaching]]></category>
            <category><![CDATA[maturity]]></category>
            <dc:creator><![CDATA[Nick Brown]]></dc:creator>
            <pubDate>Wed, 14 Aug 2024 12:19:22 GMT</pubDate>
            <atom:updated>2024-08-14T12:19:22.754Z</atom:updated>
            <content:encoded><![CDATA[<h3>Flow, value, culture, delivery — measuring agility at ASOS (part 2)</h3><p><em>The second in a two-part series where we share how at ASOS we are measuring agility across our teams and the wider tech organisation…</em></p><h3>Recap on part one</h3><p>For those who didn’t get a chance to read <a href="https://medium.com/asos-techblog/flow-value-culture-delivery-measuring-agility-at-asos-com-part-1-ee6ec01ea159">part one</a>, previously I introduced our holistic measurement to agility, covering four themes of:</p><ul><li><em>Flow </em>— the movement of work through a teams workflow/board.</li><li><em>Value</em> — the outcomes/impact in what we do and alignment with the goals and priorities of the organisation.</li><li><em>Culture</em> — the mindset/behaviours we expect around teamwork, learning and continuous improvement.</li><li><em>Delivery </em>— the practices we expect that account for the delivery of Epics/Features, considering both uncertainty and complexity.</li></ul><p>Now let’s get to explaining how the results are submitted/visualized, what the rollout/adoption has been like along with our learnings and future direction.</p><h3>Submitting the results</h3><p>We advise teams to submit a new assessment every six to eight weeks, as experience tells us this gives enough time to see the change in a particular theme. When teams are ready to submit, they go to an online Excel form and add a new row, then add the rating and rationale for each theme:</p><figure><img alt="A screenshot of a spreadsheet showing how users enter submissions" src="https://cdn-images-1.medium.com/max/1024/1*2_akW51zQ7T7MiD-GKSf4w.png" /><figcaption>Excel online file for capturing ratings and rationale for each theme</figcaption></figure><h3>Visualizing the results</h3><h4>Team view</h4><p>By default, all teams and their current rating/trend, along with the date when the last assessment was run are visible upon opening the report:</p><figure><img alt="A screenshot of all the team self-assessment results on one page" src="https://cdn-images-1.medium.com/max/1024/1*NPrA1RzJJ36hkibAMb0kiQ.png" /><figcaption>Note: all Team/Platform/Domain names anonymised for the purposes of this blog!</figcaption></figure><p>Viewers can then filter to view their team — hovering on the current rating provides the rationale as well as the previous rating and rationale. There is also a history of all the submitted ratings for the chosen theme over time:</p><figure><img alt="An animation showing the user filtering on the report for a specific team" src="https://cdn-images-1.medium.com/max/1024/1*9lGWCithwZmzpzc9RrytKw.gif" /><figcaption>Note: all Team/Platform/Domain names anonymised for the purposes of this blog!</figcaption></figure><p>Filters persist across pages, so after filtering you can also then click through to the <em>Notes/Actions</em> page to remind yourself of what your team has identified as the thing to focus on improving:</p><figure><img alt="An animation showing the user who has filtered on a team then viewing the improvement actions a team has documented" src="https://cdn-images-1.medium.com/max/1024/1*nLP8ke-BFZW3_N2gNfVUww.gif" /><figcaption>Note: all Team/Platform/Domain names anonymised for the purposes of this blog!</figcaption></figure><h4>Platform/Domain view</h4><p>Normally, we facilitate a regular discussion at ‘team of teams’ level which, depending on the size of an area, may be a number of teams in a platform or all the teams and platforms in a Domain:</p><figure><img alt="A screenshot showing the self-assessment results for a particular platform" src="https://cdn-images-1.medium.com/max/1024/1*pPzxW3a2ymOoxo8bcc0RTw.png" /><figcaption>Note: all Team/Platform/Domain names anonymised for the purposes of this blog!</figcaption></figure><p>This helps leaders in an area understand where the collective is at, as well as being able to focus on a particular team. It also can highlight where teams in an area learn from each other, rather than just relying on an Agile Coach to advise. Again filtering persists to allow for Leaders to have a holistic view of improvements across teams:</p><figure><img alt="A screenshot showing the improvement actions for a particular platform" src="https://cdn-images-1.medium.com/max/1024/1*SGTquUoqkJEXmPjKEixENg.png" /><figcaption>Note: all Team/Platform/Domain names anonymised for the purposes of this blog!</figcaption></figure><p>This is key for leaders as it informs them in understanding how they can support an environment towards continuous improvement and agility. For example if a team was experimenting with WIP limits to improve their rating for <em>Flow</em>, if a leader is pushing more work to them then this probably isn’t going to result in the theme improving!</p><h4>Tech Wide View</h4><p>The Tech Wide View provides an overview of the most recent submissions for all teams across the organisation. We feel this gives us the clearest ‘measurement’ and holistic view of agility in our tech organisation, with the ability to hover on a specific theme to see if ratings are improving:</p><figure><img alt="An animation show the tech wide view of results for the four themes and the trend" src="https://cdn-images-1.medium.com/max/1024/1*X6YvjMUNcUXJRei8cGDUaA.gif" /></figure><p>As coaches, this also helps inform us as to what practices/coaching areas we should be focusing on at scale, rather than trying to go after everything and/or focusing on just a specific team.</p><p>In turn we can use this data to help inform things like our own Objectives and Key Results (OKRs). We use this data to guide us on what we should be focusing on and, more importantly, if we are having impact:</p><figure><img alt="A screenshot showing the OKRs for the Agile Coaching team and how they use the data from the self-assessment in their key results" src="https://cdn-images-1.medium.com/max/1024/1*DMbD4Shnj3dMQsTska2Png.png" /></figure><h3>Rollout, adoption and impact</h3><p>In rolling this out, we were keen to stick to our principles and <em>invite</em> teams to complete this, rather than mandating (i.e. <em>inflicting</em>) it across all of our technology organisation. We used various channels (sharing directly with teams, presenting in Engineering All Hands, etc.) to advertise and market it, as well as having clear documentation around the assessment and the time commitment needed. After launching in Jan, this is the rate at which teams have submitted their first assessment:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*Kb_xBSi7KuNEJiFTiWQp-Q.png" /></figure><p>In addition to this, we use our internal Team Designer app (more on this <a href="https://medium.com/asos-techblog/how-you-can-grow-a-healthy-team-culture-like-ted-with-petals-acf16f982ceb">here</a>) to cross-reference our coverage across domains and platforms. This allows us to see in which areas adoption is good and in which areas we need to remind/encourage folks around trialling it:</p><figure><img alt="A screenshot of the Team Designer tool showing the percentage of teams in a platform/domain that have completed a self-assessment" src="https://cdn-images-1.medium.com/max/1024/1*jZ1vE3yyn9Bn-0verwYYSw.png" /><figcaption>Note: numbers may not match due to date screenshots were taken!</figcaption></figure><p>With any ‘product’, it’s important to consider what are appropriate <a href="https://bjpcjp.github.io/pdfs/prodmgmt/List_of_Product_Metrics.pdf">product metrics</a> to consider, particularly as we know measurable changes in behaviour from users are typically what correlate with value. One of the ways to validate if the self-assessment is adding value for teams is if they continue to use it. One-off usage may give them some insight but if it doesn’t add value, particularly with something they are ‘invited’ to use, then it will gradually die and we won’t see teams continue to complete it. Thankfully, this wasn’t the case here, as we can see that <strong>89% of teams (70 out of 79) have submitted more than one self-assessment</strong>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0Wx_xLa1Q3dF79Dj4hJndA.png" /></figure><p>The main thing though that we are concerned with in demonstrating the impact/value of this approach is if teams are actually improving. You could still have plenty of teams adopt the self-assessment yet stay the same for every rating and never actually improve. Here we visualise each time a team has seen an improvement between assessments (note: teams are only counted the first time they improve, not counted again if they improve further):</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*m8HiI0371EtOxgxxrVVTnQ.png" /></figure><p>Overall we can see the underlying story is that the vast majority of teams are improving, specifically that <strong>83% of teams (58 out of 70) who have submitted &gt;1 assessment have improved in one (or more) theme.</strong></p><h3>Learnings along the way</h3><h4>Invitation over infliction</h4><p>In order to change anything around ways of working in an organisation, teams have to want to change or “opt-in”. Producing an approach without the acceptance going in that you may be completely wrong and being prepared to ditch it leads to <a href="https://www.behavioraleconomics.com/resources/mini-encyclopedia-of-be/sunk-cost-fallacy/">sunk cost fallacy</a>. It is therefore important with something like this that teams can “opt-out” at any time.</p><h4>Keep it lightweight yet clear</h4><p>There are many agility assessments that we have seen in different organisations/the industry over the years and almost always these fall foul of not being lightweight. You do not need to ask a team 20 questions to “find out” about their agility. Having said this, lightweight is not an excuse for lack of clarity, therefore supporting documentation on how people can find out where they are or what the themes mean is a necessity. We used a Confluence page with some fast-click links to specific content to allow people to quickly get to what they needed to get to:</p><figure><img alt="A screenshot showing a Confluence wiki with quick links" src="https://cdn-images-1.medium.com/max/1024/1*45oDHqHbSJeCyAl9wkEKgg.png" /></figure><h4>Shared sessions and cadence</h4><p>Another way to increase adoption is to have teams review the results together, rather than just getting them to submit and then that’s it. In many areas we, as coaches, would facilitate a regular self-assessment review for a platform or domain. In this each team would talk through their rationale for a rating whilst the others can listen in and ask questions/give ideas on how to improve. There have been times for example when ratings have been upgraded due to teams feeling they were being too harsh (which surprisingly I also agreed with!) but the majority of time there are suggestions they can make to each other. In terms of continuous improvement and learning this is way more impactful than hearing it from just an Agile Coach.</p><h4>Set a high bar</h4><p>One of the observations we made when rolling this out was how little ‘green’ there was in particular themes. This does not automatically equate to teams being ‘bad’, more just they are where we think good is from an agility perspective, relevant to our experience and industry trends.</p><p>One of the hard parts with this is not compromising in your view of what good looks like, even though it may not be a message that people particular like. We leaned heavily on the experience of <a href="https://medium.com/u/8e121d4f8a8d">Scott Frampton</a> and <a href="https://medium.com/asos-techblog/engineering-operational-excellence-at-asos-scale-part-i-c0974c1e6dc9">his work at ASOS</a> to stay true to this, even if it at times it made for uncomfortable viewing.</p><h4>Make improvements visible</h4><p>Initially, the spreadsheet did not contain the column about what teams are going to do differently as a result of this, it was only after a brief chat with Scott and with his learnings we implemented this. Whilst it does rely on teams to add in the detail about what they are going to do differently, it helps see that teams are identifying clear action to take, based on the results of the assessment.</p><h4>Culture trumps all when it comes to improvement</h4><p>This is one of the most important things when it comes to this type of approach. One idea I discussed with <a href="https://medium.com/u/399e744ccd6f">Dan Dickinson</a> from our team was around a ‘most improved’ team, where a team had improved the most from their initial assessment to what they are now. In doing this one team was a clear standout, yet they remained at a value of ‘red’ for culture. This isn’t the type of team we should be celebrating, even if all the other factors have improved. Speedy delivery of valuable work with good rigour around delivery practices is ultimately pointless if people hate being in that team. All factors to the assessment are important but ultimately, you should <strong>never neglect culture</strong>.</p><h4>Measure it’s impact</h4><p>Finally, focus on impact. You can have lots of teams regularly assessing but ultimately, if it isn’t improving the way they work it is wasted effort. Always consider how you will validate that something like an assessment can demonstrate tangible improvements to the organisation.</p><h3>What does the future hold?</h3><p>As a coaching team we have a quarterly cadence of reviewing and tweaking the themes and their levels, sharing this with teams when any changes are made:</p><figure><img alt="A screenshot of a Confluence page showing change history for the self-assessment" src="https://cdn-images-1.medium.com/max/790/1*s8_s4acAFKkSJoKWo_ZS4A.png" /></figure><p>Currently, we feel that we have the right balance in the number of themes vs. the light-weightiness of the self-assessment. We have metrics/tools that could bring in other factors, such as <a href="https://medium.com/asos-techblog/objectively-measuring-predictability-469c654fdbcb">predictability</a> and/or quality:</p><figure><img alt="A screenshot showing a Process Behaviour Chart (PBC) for work in progress and a chart showing the rate of bug completion for a team" src="https://cdn-images-1.medium.com/max/1024/1*BycteJJi1_6WJZZe4jxmiA.png" /><figcaption>Left — a process behaviour chart highlighting where WIP has become unpredictable | Right — a chart showing the % of Throughput which are Bugs which could be a proxy for ‘quality’</figcaption></figure><p>Right now we’ll continue the small tweaks each quarter with an aim to improve as many teams, platforms and domains as we can over the next 12 months…watch this space!</p><h3>About Me</h3><p><em>I’m Nick, Lead Agile Coach at ASOS. I help guide individuals, teams and platforms to improve their ways of working in a framework-agnostic manner. Outside of work, me and my wife Nisha have a one-year old that keeps us on our toes agility-wise…</em></p><figure><img alt="A family photo of Nick, his wife Nisha and son Harrison" src="https://cdn-images-1.medium.com/max/630/0*D1BvWJILpqJ-worR.jpeg" /></figure><p><strong><em>ASOS are hiring across a range of roles in Tech. See </em></strong><a href="https://www.asoscareers.com/job-search"><strong><em>all our open positions</em></strong></a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c38bfa29ef5b" width="1" height="1" alt=""><hr><p><a href="https://medium.com/asos-techblog/flow-value-culture-delivery-measuring-agility-at-asos-part-2-c38bfa29ef5b">Flow, value, culture, delivery — measuring agility at ASOS (part 2)</a> was originally published in <a href="https://medium.com/asos-techblog">ASOS Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Flow, value, culture, delivery — measuring agility at ASOS.com (part 1)]]></title>
            <link>https://medium.com/asos-techblog/flow-value-culture-delivery-measuring-agility-at-asos-com-part-1-ee6ec01ea159?source=rss----6757df096022---4</link>
            <guid isPermaLink="false">https://medium.com/p/ee6ec01ea159</guid>
            <category><![CDATA[agile]]></category>
            <category><![CDATA[maturity]]></category>
            <category><![CDATA[culture]]></category>
            <category><![CDATA[scaled-agile]]></category>
            <category><![CDATA[agility]]></category>
            <dc:creator><![CDATA[Nick Brown]]></dc:creator>
            <pubDate>Tue, 23 Jul 2024 09:34:02 GMT</pubDate>
            <atom:updated>2025-01-24T15:48:29.040Z</atom:updated>
            <content:encoded><![CDATA[<h3>Flow, value, culture, delivery — measuring agility at ASOS (part 1)</h3><p><em>The first in a two-part series where we share how at ASOS we are measuring agility across our teams and the wider tech organisation. This part covers the problems we were looking to solve, what the themes are, as well as exploring the four themes in detail…</em></p><h3>Context and purpose</h3><p>As a team of three coaches with ~100 teams, understanding where to spend our efforts is essential to be effective in our role. Similarly, one of our main reasons to exist in ASOS is to help understand the agility of the organisation and clearly define ‘what good looks like’.</p><p>Measuring the agility of a team (and then doing this at scale) is a difficult task and in lots of organisations this is done through the usage of a <a href="https://en.wikipedia.org/wiki/Maturity_model">maturity model</a>. Whilst maturity models can provide a structured framework, they often fall short in addressing the unique dynamics of each organisation, <a href="https://www.techtarget.com/searchitoperations/opinion/DevOps-Research-and-Assessment-team-dispels-maturity-model-myths">amongst many other reasons</a>. The rigidity of these models can lead to a checklist mentality, where the focus is on ticking boxes (i.e. ‘agile compliance’) rather than fostering genuine agility. Similarly they assume everyone follows the same path where we know context differs.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*H_KnaHbaomSMTjyTh53XoQ.png" /><figcaption>Such as this…</figcaption></figure><p>Unfortunately, we also found we had teams at ASOS that were focusing on the wrong things when it comes to agility such as:</p><ul><li>Planned vs. actual items per sprint (say-do ratio)</li><li>How many items ‘spill over’ to the next sprint</li><li>How many story points they complete / what’s our velocity / what is an “8-point story” etc.</li><li>Do we follow all the agile ceremonies/events correctly</li></ul><p><strong>When it comes to agility, these things do not matter.</strong></p><p>We therefore set about developing something that would allow our teams to self-assess, which focuses on the <em>outcomes</em> agility should lead to. With the main problems to solve being:</p><ul><li>Aligning ASOS Tech on a common understanding of what agility is</li><li>Giving teams a lightweight approach to self-assess, rather than relying on Agile Coaches to observe and “tell them” how agile they are</li><li>Having an approach that is more up to date with industry trends, rather than how people were taught Scrum/Kanban 5, 10 or 15+ years ago</li><li>Having an approach to self-assessment that is framework agnostic yet considers our ASOS context</li><li>Allowing senior leaders to be more informed about where their teams are agility wise</li></ul><p>Our overarching principle being that this is a tool to inform our collective efforts towards continuous improvement, and not a tool to compare teams, nor to be used as a stick to beat them with.</p><h3>The Four Themes</h3><p>In the spirit of being lightweight, we restricted ourself to just four themes, these being things that we think are most important for teams when it comes to the outcomes you should care about when it comes to your way of working.</p><ul><li><strong><em>Flow</em></strong> — the movement of work through a teams workflow/board.</li><li><strong><em>Value</em></strong> — focusing on the outcomes/impact of what we do and alignment with the goals and priorities of the organisation.</li><li><strong><em>Culture</em></strong> — the mindset/behaviours we expect around teamwork, learning and continuous improvement.</li><li><strong><em>Delivery</em> </strong>— the practices we expect that account for the delivery of features and epics, considering both uncertainty and complexity.</li></ul><p>Each theme has three levels, which are rated on a Red/Amber/Green scale — mainly due to this being an <a href="https://medium.com/asos-techblog/engineering-operational-excellence-at-asos-scale-part-i-c0974c1e6dc9">existing scale of self-assessment</a> in other tools our engineering teams have at their disposal.</p><h4>Flow</h4><p>The focus here is around flow metrics, specifically Cycle Time and Work Item Age, with the three levels being:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*epO3Imr5-pdAYGzevlio8g.png" /></figure><p>Teams already have flow metrics available to them via a Power BI app, so are able to quickly navigate and understand where they are:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*_052uEsHImtsT714wbTGnw.png" /></figure><p>The goal with this is to make teams aware just how long items are taking, as well as how long the “in-flight” items have actually been in progress. The teams that are Green are naturally just very good at breaking work down, and/or have already embedded looking at this on a regular basis into their way of working (say in retrospectives). Those teams that are at Red/Amber have since adopted techniques such as automating the age of items on the kanban board to highlight aging items which need attention:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/794/1*wzIha2epm2TyKvMpU5F8Kg.png" /></figure><h4>Value</h4><p>The focus with this theme is understanding the impact of what we do and ensuring that we retain alignment with the goals and priorities of the organisation:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*nMtOphv6AqzSlOi3dBay2w.png" /></figure><p>In case you haven’t read it, <a href="https://medium.com/asos-techblog/measuring-value-through-portfolio-alignment-70ca2ecc7180">in the ASOS Tech blog previously</a> I’ve covered how we go about measuring portfolio alignment in teams. It essentially is looking at how much of a team backlog goes from PBI/User Story &gt; Feature &gt; Epic &gt; Portfolio Epic. We visualise this in a line chart, where teams can see the trend as well as flipping between viewing their whole backlog vs. just in-flight work:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*uh6G8Vy6SpdQfnxNtjbkMg.gif" /></figure><p>Similar to flow metrics, teams can quickly access the Power BI app to understand where they are for one part of value:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*lXhjlIuvr-26x_xTyCj7og.png" /></figure><p>The second part is where many teams currently face a challenge. Understanding the impact (value) of what you deliver is essential for any organisation that truly cares about agility. We’re all familiar with <a href="https://cutle.fish/blog/12-signs-youre-working-in-a-feature-factory">feature factories</a>, so this was a deliberate step change to get our teams away from that thinking. What teams deliver/provide support for varies, from customer facing apps to internal business unit apps and even tools or components that other teams consume, so having a ‘central location’ for looking at adoption/usage metrics is impossible. This means it can take time as either data is not readily available to teams or they had not actually really considered this themselves, most likely due to being a component part of a wider deliverable.</p><p>Still, we’ve seen good successes here, such as our AI teams who measure the impact of models they build around personalised recommendations, looking at reach and engagement. Obviously for our Web and Apps teams we have customer engagement/usage data, but we also have many teams who serve other teams/internal users, like our Data teams who look at impact in terms of report open rate and viewers/views of reports they build:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/942/1*BiLKAPb3xJ7wvkn_wW77LA.png" /></figure><h4>Culture</h4><p>Next we look at the behaviours/interactions a team has around working together and continuously improving:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0VvX8FidA6JLdIzg7iyLrg.png" /></figure><p>Ultimately, we’re trying to get away from the idea that many have around agility that continuous improvement = having retrospectives. These are meaningless if they are not identifying actionable (and measurable) improvements to your way of working, no matter how “fun” it is to do a barbie themed format!</p><p>We aren’t prescriptive in what team health tool teams use, so long as they are doing it. This could be our internal tool, PETALS (<a href="https://medium.com/asos-techblog/how-you-can-grow-a-healthy-team-culture-like-ted-with-petals-acf16f982ceb">more on this here</a>), the well known Spotify Team/Squad Health Check or even the Team Assessment built into the <a href="https://marketplace.visualstudio.com/items?itemName=ms-devlabs.team-retrospectives">retrospectives tool in Azure DevOps</a>:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gM4fzrH38Vm1QCXpb6YDIg.png" /><figcaption>All tools welcome!</figcaption></figure><p>The point is that our good teams are regularly tracking this and seeing if it is getting better.</p><p>A good example of what we are looking for at ‘green’ level is from this team who recently moved to pairing (shout out to <a href="https://medium.com/u/115e80514218">Doug Idle</a> and his team). Around 8 weeks before this image was taken they moved to pairing, which has not only made them happier as a team, but has clearly had a measurable impact in reducing their 85th percentile cycle time by 73%:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*gk0HUcZjmi6uuGYm8PW_ZA.png" /><figcaption>73% reduction (from 99 to 27 days) in 85th percentile Cycle Time as a result of pairing</figcaption></figure><p>Combine this with then sharing more widely, primarily so teams can learn from each other, then this is what we are after in our strongest teams culturally.</p><h4>Delivery</h4><p>The final theme touches on what many agile teams neglect which is ultimately about delivering. When we mean delivery in this context we’re focusing on delivery of Features/Epics (as opposed to PBI/Story level). Specifically, we believe it’s understanding risk/uncertainty and striving towards predictability and what this means when using agile principles and practices:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ryLuTNnXSjFGDddxywASJQ.png" /></figure><p>The good teams in this theme understand that due to software development being complex, you need to forecast delivery with a percentage confidence, and do this regularly. This means using data which, for our teams is available to them within a few clicks, here they can forecast, given a count of items when will they be done or, given a time box, what can they deliver.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4s3TvJnU1CqOilTL2DL8Cw.png" /></figure><p>Many teams have multiple features in their backlog, thus to get to ‘green’ our teams should leverage Feature Monte Carlo so the range of outcomes that could occur for multiple Features is visible:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*P6Gk1x_ijr3t_Ca-hzhV3g.gif" /><figcaption>Note: Feature list is fictional/not from any actual teams</figcaption></figure><p><a href="https://medium.com/asos-techblog/framework-agnostic-capacity-planning-at-scale-1f2effada94c">Previously I’ve covered our approach to capacity planning and right-sizing</a>, where teams focus on making Features no bigger than a certain size (batch) and thus can quickly (in seconds) forecast the amount of right-sized features they have capacity for, which again is what we look for in our ‘green’ criteria:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*TNNO9e_20-8w4QeMRr1jFA.png" /><figcaption>Note: Feature list is fictional/not from any actual teams</figcaption></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*2yjwn7QKL2skqLLhg2lPqA.gif" /></figure><p>The best way to do this really is to have a regular cadence where you specifically look at delivery and these particular metrics, that way you’re informed around your progress and any items that may need breaking down/splitting.</p><h3>Part two…</h3><p>In <a href="https://medium.com/asos-techblog/flow-value-culture-delivery-measuring-agility-at-asos-part-2-c38bfa29ef5b">this post</a> I share how teams submit their results (and at what cadence) as well as how the results are visualised, what the rollout/adoption has been like, along with our learnings and future direction for the self-assessment.</p><h3>About Me</h3><p><em>I’m Nick, Lead Agile Coach at ASOS. I help guide individuals, teams and platforms to improve their ways of working in a framework-agnostic manner. Outside of work, me and my wife Nisha have a one-year old that keeps us on our toes agility-wise…</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/630/0*D1BvWJILpqJ-worR.jpeg" /></figure><p><strong><em>ASOS are hiring across a range of roles in Tech. See </em></strong><a href="https://www.asoscareers.com/job-search"><strong><em>all our open positions</em></strong></a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ee6ec01ea159" width="1" height="1" alt=""><hr><p><a href="https://medium.com/asos-techblog/flow-value-culture-delivery-measuring-agility-at-asos-com-part-1-ee6ec01ea159">Flow, value, culture, delivery — measuring agility at ASOS.com (part 1)</a> was originally published in <a href="https://medium.com/asos-techblog">ASOS Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Here’s what we learned at Google I/O Connect Berlin]]></title>
            <link>https://medium.com/asos-techblog/heres-what-we-learned-at-google-i-o-connect-berlin-503163f8139c?source=rss----6757df096022---4</link>
            <guid isPermaLink="false">https://medium.com/p/503163f8139c</guid>
            <category><![CDATA[android]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[apps]]></category>
            <category><![CDATA[tech]]></category>
            <dc:creator><![CDATA[Ed Holloway-George]]></dc:creator>
            <pubDate>Wed, 17 Jul 2024 08:15:39 GMT</pubDate>
            <atom:updated>2025-10-02T07:57:47.205Z</atom:updated>
            <content:encoded><![CDATA[<h3>Learnings from Google I/O Connect Berlin</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*7v181ROw_p_-xYoEkATKoA.jpeg" /><figcaption>📸 : <a href="https://medium.com/u/1e7c83978135">Ed Holloway-George</a></figcaption></figure><p><em>This month, Google hosted the first of its follow-up series of events for their annual Google I/O conference. In this post, </em><a href="https://sp4ghetticode.medium.com/"><em>Ed Holloway-George</em></a><em> highlights what he learned at the I/O Connect event held in Berlin, Germany.</em></p><p>As an Android developer and self-proclaimed tech nerd, the <a href="https://en.wikipedia.org/wiki/Google_I/O">Google I/O conference</a> is one of the dates I <em>always</em> look forward to on my calendar each year.</p><p>Held at the impressive Shoreline Amphitheatre in Mountain View, California, it’s usually Google’s primary showcase of all the exciting announcements they have to share across their software, hardware and other digital products. In years gone by, we’ve seen major announcements including the launch of Google Assistant, Google Home, numerous Android versions and of course, the ill-fated and slightly ahead of its time, <a href="https://en.wikipedia.org/wiki/Google_Glass">Google Glass</a>.</p><p>However, this year’s edition held on May 14th had from the offset a <em>very specific </em>focus — <strong>AI</strong>. As the hottest topic in tech right now, it comes as no surprise that Google is also going all-in with its <a href="https://ai.google/">AI offerings</a> this time around. In fact, so much so, that “AI” was mentioned <a href="https://techcrunch.com/2024/05/14/google-mentioned-ai-120-times-during-its-i-o-keynote">at least 120 times</a> during the main keynote presentation 😅 That aside, there were also many other announcements in the mobile, cloud and web spaces for developers and consumers to look forward to. If you missed any of these yourself, you should check out <a href="https://io.google/2024/">the official site</a> and catch up 👀</p><p>Whilst the main conference was primarily focused on announcements, Google also has several smaller developer-focused follow-up events known as I/O Connect. The idea behind I/O Connect events is to get the new technologies announced at the main conference in the hands of developers, have Google employees (Googlers) readily available to take questions as well as present additional content and of course, for developers from across the world to meet up and share a common passion for Google’s tech.</p><p>Being a <a href="https://developers.google.com/profile/u/spaghetticode">Google Developer Expert for Android</a>, I was fortunate enough to be invited to this year’s European instalment of I/O Connect, held in a swelteringly hot Berlin 🤒</p><p>So, to completely misquote the Ramones, “I/O let’s go!”…</p><figure><img alt="An artificial indoors river with a boat in the foreground and lush green riverbanks and a white bridge with the Google I/O Connect logo on spanning it" src="https://cdn-images-1.medium.com/max/1024/1*tX2npGr037PlrDTj35E8Xw.jpeg" /><figcaption>I’d never seen an artificial indoor river before either!</figcaption></figure><h4>What I learned at I/O Connect 24</h4><p>In my experience, there’s no such thing as a dull Google event. I/O Connect this year comes as no exception with an indoor artificial river installation being the centrepiece of the event’s location within a converted warehouse complex. With multiple auditoriums, workshop spaces and free barista-made coffee, the stage was <em>already</em> set for a great developer event.</p><p>But what did we learn? There were <a href="https://developers.googleblog.com/en/bringing-the-io-magic-to-berlin/">some amazing announcements</a>, but here’s a summary of some of my personal key takeaways!</p><ul><li><strong>Google’s investment in AI is scaling up</strong></li></ul><p>Google used I/O Connect’s keynote to <a href="https://blog.google/technology/developers/google-gemma-2/">announce their latest “Gemma 2” suite of models</a>. The newly available models boast the ability to support either 9 billion or 27 billion parameters and have performance metrics that surpass known industry leaders such as the popular Llama-3 offering from Meta. The rapid release of Google’s AI products and the mind-boggling numbers associated with them is a testament to how much effort they are focusing on this area and it continues to be at the forefront of developer’s minds.</p><p>A live demo of Google’s Gemini AI being used in developer tooling to aid the investigation of a bug in software was incredibly popular with the Keynote’s crowd and despite another AI live demo going a bit wrong, the sentiment with most is that AI is not just something to benefit end-users but something that can be used at <strong>all stages</strong> of software development.</p><p>Like many tech-centric companies, we are no strangers to AI’s benefits as we’ve previously blogged about a <a href="https://medium.com/asos-techblog/transforming-recommendations-at-asos-254b95c6a07a">real-world application of AI/ML</a> used here at ASOS and also our process of looking into <a href="https://medium.com/asos-techblog/asos-gen-ai-hack-day-a-tale-of-innovation-b1583ab545dd">new ways to use Gen AI through hackathons</a>. Google’s recent announcements have all but reinforced our belief that the future is AI ✨</p><ul><li><strong>Kotlin Multiplatform continues to thrive</strong></li></ul><p><a href="https://www.jetbrains.com/kotlin-multiplatform/">Kotlin Multiplatform (KMP)</a> are the words on many mobile developer’s lips right now. The ability to effectively share code between iOS, Android, Desktop and Web sounds almost too good to be true, but the KMP model is one that is growing in popularity and shows no signs of slowing down its adoption at scale.</p><p>Google recently announced a series of changes to some of their core <a href="https://developer.android.com/kotlin/multiplatform">Android ‘Jetpack’ libraries</a> that allows their popular libraries such as ViewModel, Paging and Room to now be used in a KMP context. I/O Connect was an opportunity to revisit these announcements, see them in action and have a discussion with Googlers about their future plans for KMP support. As it was made clear in their presentations, Google is now fully endorsing ‘KMP for shared business logic between Android &amp; iOS’ which is an exciting prospect for many mobile developers.</p><p>I am pleased to reveal that our mobile teams at ASOS have recently been exploring KMP for shared business logic between our iOS and Android apps and have been actively trialling the use of shared ASOS KMP libraries in our most recent Android release (with iOS coming very soon). If you’d like to find out more about how this went, our approach and results, then perhaps keep an eye out for a future blog post! 👀</p><ul><li><strong>Performance in mobile is still key</strong></li></ul><p>Another key area of focus within Google’s Android ecosystem right now is that of app performance. I/O Connect was no exception with hands-on workshops and tech-talks focussing on performance libraries such as <a href="https://developer.android.com/topic/performance/baselineprofiles/overview">Baseline Profiles</a> and <a href="https://developer.android.com/topic/performance/baselineprofiles/measure-baselineprofile">Macrobenchmarks</a>.</p><p>This was an awesome opportunity to chat, raise any issues and pair program directly with the Googlers involved in these libraries and had great feedback from all involved. Sadly, a conflict with another event commitment meant I wasn’t able to attend directly — but I am told it was incredibly useful!</p><p>App performance is something we are always looking to improve at ASOS and I am pleased to say we have a roadmap to introduce these libraries into the Android app in the very near future. Watch out Usain Bolt, we are coming for your speed record 🏃‍♂️💨</p><ul><li><strong>The wider tech community continues to be impressive</strong></li></ul><p>With over 100 countries represented from across Europe, the Middle East and Africa, Google’s I/O Connect event embodied the very best of the tech community and brought together people who all share a common passion; <em>To make the world a better place through technology.</em></p><p>ASOS is one of many companies that actively work with cutting-edge technologies, such as those announced at I/O, to deliver better results and experiences to our customers. We’ll continue to keep on top of the rapidly changing tech landscape and we are all excited to share more about our advancements in these areas in future posts!</p><p>In closing, I hope you found this insightful and join me in looking forward to seeing what else comes from the remaining I/O Connect events in Bengaluru — India and Beijing — China later this summer.</p><p>I hope to catch <strong>you</strong> at next year’s events wherever they might be! 🗺️</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hDKI8VlHwLK2q2fMXjeLhg.jpeg" /></figure><p><em>Hi, I am </em><a href="https://linktr.ee/sp4ghetticode"><em>Ed Holloway-George</em></a><em> an Android Lead here at ASOS and Google Developer Expert for Android. I joined the company in 2021 and when I’m not sweating buckets at developer events abroad, I can be found </em><a href="https://www.spght.dev/talks/"><em>talking</em></a><em> about Mobile Security or </em><a href="https://twitter.com/sp4ghetticode"><em>tweeting</em></a><em> pictures of my dog </em>🐶</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=503163f8139c" width="1" height="1" alt=""><hr><p><a href="https://medium.com/asos-techblog/heres-what-we-learned-at-google-i-o-connect-berlin-503163f8139c">Here’s what we learned at Google I/O Connect Berlin</a> was originally published in <a href="https://medium.com/asos-techblog">ASOS Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Observations from the London DevOps Conference 2024]]></title>
            <link>https://medium.com/asos-techblog/observations-from-the-london-devops-conference-2024-91d034502e10?source=rss----6757df096022---4</link>
            <guid isPermaLink="false">https://medium.com/p/91d034502e10</guid>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[agile]]></category>
            <category><![CDATA[conference]]></category>
            <category><![CDATA[coaching]]></category>
            <category><![CDATA[metrics]]></category>
            <dc:creator><![CDATA[Dan Dickinson]]></dc:creator>
            <pubDate>Tue, 07 May 2024 09:28:51 GMT</pubDate>
            <atom:updated>2024-05-07T09:28:51.183Z</atom:updated>
            <content:encoded><![CDATA[<p><em>Sharing knowledge and understanding is a key component of building a safe and healthy culture. Within the Agile Coaching Team at ASOS, we have agreed to share our experiences, not only with one another, but also the wider tech community to further support continuous improvement and learning. Here are the learnings </em><a href="https://www.linkedin.com/in/esha-bakshi-8360"><em>Esha</em></a><em> and I gained from a recent conference we attended, DevOps 2024…</em></p><p><strong>Melissa Perri — Becoming Truly Product Led</strong></p><p>The day kicked off with the anticipated keynote by <a href="https://melissaperri.com/">Melissa Perri</a>; who delved into essential topics such as product strategy and operations, sharing anecdotes and real-world examples from her extensive industry experience and expertise. Melissa highlighted that companies are just doing too much and not focusing on the right thing for their customers. This has led to the tech industry experiencing 28k layoffs since 2023! From this, it is clear how important it is for organisations to have a product portfolio strategy and recognising that the product IS the business.</p><blockquote>The mention of product operations (<a href="https://melissaperri.com/book">The discipline of helping your product management function scale well. Surrounding the team with all of the essential inputs to set strategy, prioritize, and streamline ways of working</a>) sparked our interest, it is centred around data and enabling product teams to see everything they need to (something we’re REALLY passionate about as coaches!). Making it possible for self-serve data to exist will allow people to do research, make informed decisions and measure the impact of product initiatives.</blockquote><ol><li>Business and data insights — get the information out of systems and into the hands of people making decisions.</li><li>Customer and market insights — aggregate external research</li><li>Process and governance — leverage the skills and information for seamless collaboration</li></ol><p>Product operations helps to translate a company strategy and goals into an actionable roadmap. Clearly mapping out an organisation’s planning processes is key for creating a balance between governance and agility.</p><p><strong>Planning enablers</strong></p><ul><li>Design the cadence of inputs, decisions and communications</li><li>Generate alignment in initiatives</li><li>Bring together the right product people and stakeholders for decision making</li><li>Define key meetings: Set a fast-paced agenda with clearly defined meeting outcomes</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GewmRd_WfJe4AKcDWSks4g.png" /><figcaption><em>Oscar Health’s anatomy of planning</em></figcaption></figure><p>We were so inspired by the talk we rushed to get a signed copy of Melissa’s new book ‘<a href="https://www.productoperations.com/">Product Operations — How successful companies build better products at scale</a>’. A huge highlight for Dan 😊</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*XuMTxdH59vXPfD74Nv-6_w.png" /></figure><p><strong>Matthew Skelton — DevOps Topologies 10 Years on. What have we learned about silos, collaboration and flow</strong></p><p>After a short break we attended Matthew Skelton’s talk on <a href="https://teamtopologies.com/">Team Topologies</a> and his observations from the last 10 years. An interesting point and theme which appeared throughout the talk was the linking between Fast Flow (Quickly developing and releasing) and Cognitive Load (The amount of memory resources that are used or required for mental processes), Matthew clarified that success lies in addressing dynamic interactions, not building a perfect static structure.</p><p>Cognitive load appeared throughout the day in a variety of formats, however it’s clear the focus on Developer Experience (DevEx) is becoming more prudent within the industry. To support this, Matthew mentioned “The cost of tangled software” and the staggering impact (£100,000 per dev/year) of blocking engineers one hour a day on the operational cost of a company. To improve flow, we need to untangle our organisations, which in turn will decouple the software developed.</p><p>Matthew mentioned key metrics that team topologies has led to organisations uncovering as good measures to use around agility:</p><p><strong>Key metrics (and what they monitor)</strong></p><ul><li><strong>Lead time — Flow</strong> (Elapsed time between the identification of a requirement and its fulfilment)</li><li><strong>Deployment Frequency — Flow </strong>(How frequently new code is deployed to production)</li><li><strong>Mean time to restore — Operability</strong> (Average time taken to recover a product or system failure)</li><li><strong>Change fail percentage — Operability </strong>(% of changes which lead to production failures)</li><li><strong>% of time building vs waiting — Flow Efficiency</strong> (% of time spent actively working on items vs waiting/blocked)</li></ul><p>Understanding the problem — Matthew mentioned both <a href="https://learnwardleymapping.com/%22%20/l%20%22learn-more">Wardley mapping</a> and <a href="https://www.atlassian.com/blog/teamwork/what-is-conways-law-acmi">Conway’s Law</a> to highlight how complex org structures lead to highly complex products, which in turn cause increased cognitive load of developers. The greater the cognitive load on developers, the slower they are going to build and have a greater chance of unknowingly impacting another product or service.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*GlL6BpErjzmvwzpAJ-4HqA.jpeg" /></figure><p>Matthew provided an example where J P Morgan applied team topologies by mapping dependencies between teams and then actively worked to decouple teams wherever possible. This saw a 60% reduction in dependencies and a direct cost saving as teams experienced less delays. Similarly, the Home Office were able to reduce technology costs by 40% by making Product Owners account for the cost spent on their services, which instantly encouraged a cleaner split of teams and technology structure. This reinforced the message that cognitive load on developers significantly impacts flow and build and maintenance costs.</p><p><strong>Emma Dahl Jeppesen — From assumptions to measured impact: Improving developer experience at TV2</strong></p><p>Having split to cover more talks, Dan attended a session by broadcasting company TV2, which is used at least one hour a day by 85% of Danes. This session covered the topic of DevEx and how new measures have been introduced to highlight bottlenecks and pain points in the development process.</p><p><a href="https://www.linkedin.com/in/emmadj/">Emma</a> referenced how TV2 previously introduced <a href="https://dorametrics.org/#:~:text=DORA%20metrics%2C%20which%20stand%20for%20Deliverability%2C%20Operations%20and,and%20efficiency%20of%20various%20engineering%20systems%20and%20processes.">DORA metrics</a> (Deployment Frequency, Mean Lead Time for Changes, Mean Time to Recover, Change Failure Rate) across the company as a way of monitoring performance. However, since the metrics were used as targets instead of simply measuring the trends (<a href="https://builtin.com/data-science/goodharts-law">Goodhart’s Law</a>), they noticed how teams would act in certain ways to game the metrics. An example of this was teams deploying multiple smaller chunks of code which should have been deployed together, this became apparent once tech leadership started to challenge why some teams were deploying less than others.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*oDY8t1gyHuy_ZEHzuR6pbA.jpeg" /><figcaption>Developer journey at TV2</figcaption></figure><p>TV2 introduced qualitative surveys to gain a deeper understanding of how Developers are feeling and the flows work passes through before considered done. These surveys were highly welcomed and the insights allowed the organisation to focus on improving specific issues which blocked developers day to day such as streamlining the CI pipeline.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*R4mjOMRr9umo3oH8fgFU-A.jpeg" /><figcaption>TV2 Developer Experience Survey</figcaption></figure><p><strong>How can we utilise this approach ourselves?</strong></p><ul><li><a href="https://www.lean.org/the-lean-post/articles/understanding-the-fundamentals-of-value-stream-mapping/">Value stream mapping </a>— Research our developer’s journeys using both qualitative and quantitative data</li><li>Ask Devs to walk through their process and how they’re feeling at each point, highlighting painful steps</li><li>Review and update both As Is and To Be, continually measure if moving in the right direction</li><li>Aim to implement changes as quickly as possible, validate and then continually improve. Don’t spend too much time planning the change (Better to monitor and adjust as you go)</li></ul><p><strong>Kristian Lindwall — Innovating at scale — Spotify’s experimentation journey</strong></p><p>Later in the day we heard from <a href="https://www.linkedin.com/in/kristianlindwall/">Kristian Lindwall</a> as he walked through the various ways in which Spotify focus on experimentation across the entire development journey. Throughout the talk, Kristian referred to “cultivating curiosity” by creating an environment which encourages autonomy, transparency and connectivity. An example of this was Spotify’s experimentation platform (<a href="https://engineering.atspotify.com/2020/10/spotifys-new-experimentation-platform-part-1/">ABBA</a>), which enabled running tests on a far greater scale while also monitoring wider KPIs. This approach simplified how teams create and run tests, encouraging better development practices as testing was easy to do and provided insight to the wider impacts across Spotify.</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/1*MVHPTsv5CsL-45aaCG7-ww.jpeg" /></figure><p>The platform ABBA was formed from an experimental approach in Spotify to identify pain points in the dev process. By taking an experimental approach, Spotify was able to identify and focus on the largest problems. After several iterations, the platform was able to separate experiments from flags, measure metrics and coordinate experimentation within a single tool. Spotify’s confidence in the tool has enabled them to package and sell ABBA as a separate product entirely, this is something that we’ve talked about previously in this blog, such as <a href="/service/mailto:scott.frampton@asos.com">@Scott Frampton</a> (in Medium you could tag him) and the <a href="https://medium.com/asos-techblog/engineering-operational-excellence-at-asos-scale-part-i-c0974c1e6dc9">journey with fundamentals</a> or <a href="/service/mailto:callum.trounce@asos.com">@Callum Trounce</a> post on <a href="https://medium.com/asos-techblog/embracing-an-experiment-oriented-mindset-at-asos-9f53551f692e">how we embrace experimentation</a>.</p><p>Whilst this talk was heavily focussed on the experimentation platform created by Spotify, it did reinforce their constant focus on experimentation and continuous improvement. By taking time to understand the developer’s experiences, they have reduced complexity and simultaneously create a product which has introduced additional revenue streams.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*j1VaTqEKzMc_zgh4oy25kQ.jpeg" /></figure><p><strong>Sven Peters — Developer Joy — How great teams get s&amp;#t done</strong></p><p>The final talk we went to was all about improving developer experience, <a href="https://www.linkedin.com/in/svenpeters73/">Sven Peters</a> from Atlassian calls this “Developer Joy”. Over the past 10 years the cognitive load of developers has significantly increased as technology advanced and they became responsible for much more than just writing code. The question of how to measure developer productivity arose and almost has become an obsession with leaders.</p><p>We travelled back in time with Sven to remind ourselves that software development is not the same as a production line so productivity cannot be measured in the same way. At Atlassian, the team thought about what improves the developer experience and what brings them joy, measured it through satisfaction surveys and then handed over the information to the actual teams to solve (not a productivity committee!).</p><p>What they found was that if developers can work on great quality code and it can flow through with reduced waiting time as well as getting customer feedback, then developer joy emerges.</p><p>An example of changes made at Atlassian includes addressing the Quality Assurance (QA) ‘wait time’. The team found that they had a QA to developer ratio of 1:30 which created a bottleneck in their flow, so they pivoted the role to be Quality Assistance. This change in remit meant the QA role would do a kick off work with developers and coach them to carry out better testing and write test plans. This enabled the developers to carry out testing themselves, leaving the QA role to run test environments and reduce the developer cognitive load.</p><p><strong>Steps taken to improve developer joy</strong></p><ol><li>Ask your developers how satisfied they are — use surveys</li><li>Give the teams time (10–20%) to solve developer issues</li><li>Spread the learnings across teams so others can benefit from improvements</li></ol><p><strong>Key Learnings</strong></p><p>We felt there was a lot of connection between the talks we went to, one of which being that unlocking flow in an organisation and a developer’s journey creates both cultural and financial benefits.</p><p>We were reminded how important it is for successful businesses to recognise not only customer but also developer experience when creating valuable products. Further supported by the role of leaders when creating organisational structure, our thinking must span beyond the products and instead encompass the supporting structures and systems to ensure we are able to pivot quickly as needed.</p><h3>About Me</h3><p><em>I’m Dan Dickinson, an Agile Coach at ASOS. I help guide platforms and domains through our continuous improvement journey, utilising data and insights to drive better behaviours, processes and mindset. Outside of work I’m a Dog Dad, Golfer and Scuba Diver.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/755/1*rjoNJKl5VAi5Qc2vkHiT5w.jpeg" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=91d034502e10" width="1" height="1" alt=""><hr><p><a href="https://medium.com/asos-techblog/observations-from-the-london-devops-conference-2024-91d034502e10">Observations from the London DevOps Conference 2024</a> was originally published in <a href="https://medium.com/asos-techblog">ASOS Tech Blog</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>