<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Odunayo Dada</title>
    <description>The latest articles on DEV Community by Odunayo Dada (@odunayo_dada).</description>
    <link>https://dev.to/odunayo_dada</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2633308%2Fcbdf918b-3871-4a3f-ba3d-c58f4c5c192a.jpg</url>
      <title>DEV Community: Odunayo Dada</title>
      <link>https://dev.to/odunayo_dada</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/odunayo_dada"/>
    <language>en</language>
    <item>
      <title>How I Built DevTeam AI: A Multi-Agent Software Engineering Team Powered by QwenCloud</title>
      <dc:creator>Odunayo Dada</dc:creator>
      <pubDate>Wed, 08 Jul 2026 20:45:49 +0000</pubDate>
      <link>https://dev.to/odunayo_dada/how-i-built-devteam-ai-a-multi-agent-software-engineering-team-powered-by-qwencloud-40i9</link>
      <guid>https://dev.to/odunayo_dada/how-i-built-devteam-ai-a-multi-agent-software-engineering-team-powered-by-qwencloud-40i9</guid>
      <description>&lt;h1&gt;
  
  
  &lt;strong&gt;How I Built DevTeam AI: A Multi-Agent Software Engineering Team Powered by QwenCloud&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Turning a single product idea into a complete software delivery plan using specialized AI agents, human approval workflows, and QwenCloud.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Large Language Models have become incredibly good at generating code, documentation, and product ideas. But there's one problem I kept running into.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Software isn't built by one person.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A successful product typically involves product managers, solution architects, backend engineers, frontend engineers, QA engineers, technical leads, and reviewers—all collaborating, challenging assumptions, and refining ideas before a single line of production code is written.&lt;/p&gt;

&lt;p&gt;Most AI applications flatten all of these roles into a single prompt.&lt;/p&gt;

&lt;p&gt;I wanted to explore something different.&lt;/p&gt;

&lt;p&gt;For the &lt;strong&gt;QwenCloud Global AI Hackathon&lt;/strong&gt; (Agent Society Track), I built &lt;strong&gt;DevTeam AI&lt;/strong&gt;—a multi-agent software engineering platform where specialized AI agents collaborate like a real product team, complete with approvals, design reviews, negotiations, revisions, and code generation.&lt;/p&gt;

&lt;p&gt;Rather than asking one AI assistant to do everything, DevTeam AI orchestrates an entire software delivery workflow powered by &lt;strong&gt;QwenCloud&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Problem
&lt;/h1&gt;

&lt;p&gt;One of the most expensive parts of software development isn't coding.&lt;/p&gt;

&lt;p&gt;It's planning.&lt;/p&gt;

&lt;p&gt;Anyone who has worked on production software knows the journey usually looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product requirements&lt;/li&gt;
&lt;li&gt;Architecture discussions&lt;/li&gt;
&lt;li&gt;API design&lt;/li&gt;
&lt;li&gt;Database modeling&lt;/li&gt;
&lt;li&gt;Frontend planning&lt;/li&gt;
&lt;li&gt;QA strategy&lt;/li&gt;
&lt;li&gt;Security review&lt;/li&gt;
&lt;li&gt;Technical review&lt;/li&gt;
&lt;li&gt;Scope negotiations&lt;/li&gt;
&lt;li&gt;Implementation planning&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These activities often involve multiple stakeholders with conflicting priorities.&lt;/p&gt;

&lt;p&gt;A product manager wants more features.&lt;/p&gt;

&lt;p&gt;A backend engineer wants simplicity.&lt;/p&gt;

&lt;p&gt;QA wants better testability.&lt;/p&gt;

&lt;p&gt;The architect wants scalability.&lt;/p&gt;

&lt;p&gt;The CTO worries about long-term maintainability.&lt;/p&gt;

&lt;p&gt;Most AI tools merge all these viewpoints into one response.&lt;/p&gt;

&lt;p&gt;I wanted to see whether AI agents could behave more like a real engineering organization.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Idea Behind DevTeam AI
&lt;/h1&gt;

&lt;p&gt;Instead of one intelligent assistant...&lt;/p&gt;

&lt;p&gt;I built an &lt;strong&gt;AI software company.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each agent has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A clearly defined responsibility&lt;/li&gt;
&lt;li&gt;Dedicated prompts&lt;/li&gt;
&lt;li&gt;Inputs from previous agents&lt;/li&gt;
&lt;li&gt;Approval dependencies&lt;/li&gt;
&lt;li&gt;Review responsibilities&lt;/li&gt;
&lt;li&gt;Conflict resolution capabilities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rather than producing one giant response, the system produces &lt;strong&gt;a complete software delivery package&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  Meet the AI Engineering Team
&lt;/h1&gt;

&lt;p&gt;DevTeam AI currently orchestrates &lt;strong&gt;11 specialized AI agents&lt;/strong&gt; across multiple workflow stages.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Orchestrator Agent
&lt;/h2&gt;

&lt;p&gt;Coordinates the entire workflow.&lt;/p&gt;

&lt;p&gt;Responsible for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sequencing stages&lt;/li&gt;
&lt;li&gt;dependency validation&lt;/li&gt;
&lt;li&gt;workflow state&lt;/li&gt;
&lt;li&gt;execution monitoring&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2. Product Manager Agent
&lt;/h2&gt;

&lt;p&gt;Transforms a rough product idea into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product Requirements Document (PRD)&lt;/li&gt;
&lt;li&gt;User stories&lt;/li&gt;
&lt;li&gt;Acceptance criteria&lt;/li&gt;
&lt;li&gt;MVP scope&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. Solution Architect Agent
&lt;/h2&gt;

&lt;p&gt;Designs the technical foundation.&lt;/p&gt;

&lt;p&gt;Outputs include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;System architecture&lt;/li&gt;
&lt;li&gt;Component responsibilities&lt;/li&gt;
&lt;li&gt;Mermaid architecture diagrams&lt;/li&gt;
&lt;li&gt;Technology recommendations&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. Backend Engineer Agent
&lt;/h2&gt;

&lt;p&gt;Designs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;REST APIs&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;Database schema&lt;/li&gt;
&lt;li&gt;Business logic&lt;/li&gt;
&lt;li&gt;Service structure&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5. Frontend Engineer Agent
&lt;/h2&gt;

&lt;p&gt;Plans:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User flows&lt;/li&gt;
&lt;li&gt;UI architecture&lt;/li&gt;
&lt;li&gt;State management&lt;/li&gt;
&lt;li&gt;Component hierarchy&lt;/li&gt;
&lt;li&gt;Mobile/Web implementation&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  6. QA Engineer Agent
&lt;/h2&gt;

&lt;p&gt;Produces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test strategy&lt;/li&gt;
&lt;li&gt;Test cases&lt;/li&gt;
&lt;li&gt;Edge cases&lt;/li&gt;
&lt;li&gt;Risk analysis&lt;/li&gt;
&lt;li&gt;Acceptance validation&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  7. CTO Reviewer Agent
&lt;/h2&gt;

&lt;p&gt;One of my favorite agents.&lt;/p&gt;

&lt;p&gt;Instead of generating new work...&lt;/p&gt;

&lt;p&gt;It challenges existing work.&lt;/p&gt;

&lt;p&gt;It looks for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inconsistent assumptions&lt;/li&gt;
&lt;li&gt;scalability issues&lt;/li&gt;
&lt;li&gt;security risks&lt;/li&gt;
&lt;li&gt;missing requirements&lt;/li&gt;
&lt;li&gt;architectural weaknesses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Real engineering teams become stronger because of review—not because everyone agrees.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Negotiator Agent
&lt;/h2&gt;

&lt;p&gt;When agents disagree...&lt;/p&gt;

&lt;p&gt;Someone has to make a decision.&lt;/p&gt;

&lt;p&gt;The Negotiator Agent produces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;decision records&lt;/li&gt;
&lt;li&gt;trade-offs&lt;/li&gt;
&lt;li&gt;compromise recommendations&lt;/li&gt;
&lt;li&gt;rationale&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This became one of the most interesting parts of the project because it mimics real technical discussions.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Revision Coordinator
&lt;/h2&gt;

&lt;p&gt;After human feedback, this agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;summarizes requested changes&lt;/li&gt;
&lt;li&gt;coordinates revisions&lt;/li&gt;
&lt;li&gt;keeps documentation synchronized&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  10. Code Generator Agent
&lt;/h2&gt;

&lt;p&gt;Once planning has been approved, this agent generates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;starter project&lt;/li&gt;
&lt;li&gt;boilerplate&lt;/li&gt;
&lt;li&gt;folder structure&lt;/li&gt;
&lt;li&gt;implementation scaffold&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  11. Code Reviewer Agent
&lt;/h2&gt;

&lt;p&gt;Performs a final review of generated code by checking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;maintainability&lt;/li&gt;
&lt;li&gt;best practices&lt;/li&gt;
&lt;li&gt;potential bugs&lt;/li&gt;
&lt;li&gt;consistency&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Human Approval Is Not Optional
&lt;/h1&gt;

&lt;p&gt;One decision I made early was that &lt;strong&gt;AI should not automatically move to the next stage.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every stage requires human approval.&lt;/p&gt;

&lt;p&gt;The workflow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Idea

↓

PRD

↓

Architecture

↓

Backend

↓

Frontend

↓

QA

↓

CTO Review

↓

Negotiation

↓

Revision

↓

Code Generation

↓

Code Review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If something isn't right...&lt;/p&gt;

&lt;p&gt;The workflow stops.&lt;/p&gt;

&lt;p&gt;The user can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;approve&lt;/li&gt;
&lt;li&gt;regenerate&lt;/li&gt;
&lt;li&gt;request revisions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only after approval does the next stage begin.&lt;/p&gt;

&lt;p&gt;This small decision makes DevTeam AI feel much closer to a real engineering workflow than a traditional prompt chain.&lt;/p&gt;




&lt;h1&gt;
  
  
  Why I Chose QwenCloud
&lt;/h1&gt;

&lt;p&gt;When building multi-agent systems, the language model quickly becomes the least interesting part.&lt;/p&gt;

&lt;p&gt;The orchestration layer becomes the real challenge.&lt;/p&gt;

&lt;p&gt;I wanted a model that allowed me to focus on building agent collaboration rather than spending days adapting SDKs.&lt;/p&gt;

&lt;p&gt;QwenCloud was a natural fit.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenAI-Compatible API
&lt;/h2&gt;

&lt;p&gt;Integration was surprisingly straightforward.&lt;/p&gt;

&lt;p&gt;Because the API is OpenAI-compatible, I didn't need to redesign my backend.&lt;/p&gt;

&lt;p&gt;Instead, I focused on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;workflow orchestration&lt;/li&gt;
&lt;li&gt;agent prompting&lt;/li&gt;
&lt;li&gt;approval gates&lt;/li&gt;
&lt;li&gt;state management&lt;/li&gt;
&lt;li&gt;conflict resolution&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Excellent Structured Outputs
&lt;/h2&gt;

&lt;p&gt;Every agent generates structured artifacts.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Markdown&lt;/li&gt;
&lt;li&gt;JSON&lt;/li&gt;
&lt;li&gt;Mermaid diagrams&lt;/li&gt;
&lt;li&gt;API specifications&lt;/li&gt;
&lt;li&gt;PRDs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consistent formatting was extremely important because later agents depend on previous outputs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Strong Reasoning
&lt;/h2&gt;

&lt;p&gt;Many stages require analysis rather than generation.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reviewing architecture&lt;/li&gt;
&lt;li&gt;identifying contradictions&lt;/li&gt;
&lt;li&gt;comparing implementation plans&lt;/li&gt;
&lt;li&gt;detecting missing requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These reasoning-heavy tasks are where QwenCloud performed particularly well.&lt;/p&gt;




&lt;h2&gt;
  
  
  Perfect for Multi-Agent Workflows
&lt;/h2&gt;

&lt;p&gt;A multi-agent system might invoke the model dozens of times for one project.&lt;/p&gt;

&lt;p&gt;Each call has different objectives.&lt;/p&gt;

&lt;p&gt;Some agents create.&lt;/p&gt;

&lt;p&gt;Others critique.&lt;/p&gt;

&lt;p&gt;Others summarize.&lt;/p&gt;

&lt;p&gt;Others negotiate.&lt;/p&gt;

&lt;p&gt;QwenCloud handled these role-specific prompts reliably throughout development.&lt;/p&gt;




&lt;h1&gt;
  
  
  System Architecture
&lt;/h1&gt;

&lt;p&gt;The architecture intentionally keeps the infrastructure simple while making the workflow sophisticated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 User
                   │
          Next.js Frontend
                   │
        Server-Sent Events
                   │
             FastAPI Backend
                   │
        Workflow Orchestrator
                   │
     ┌────────────────────────┐
     │ Product Manager        │
     │ Architect              │
     │ Backend                │
     │ Frontend               │
     │ QA                     │
     │ CTO                    │
     │ Negotiator             │
     │ Revision               │
     │ Code Generator         │
     │ Code Reviewer          │
     └────────────────────────┘
                   │
             QwenCloud API
                   │
               SQLite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  Technology Stack
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Frontend
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Next.js&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Tailwind CSS&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Backend
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;FastAPI&lt;/li&gt;
&lt;li&gt;Python&lt;/li&gt;
&lt;li&gt;SQLite&lt;/li&gt;
&lt;li&gt;HTTPX&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  AI
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;QwenCloud&lt;/li&gt;
&lt;li&gt;OpenAI-compatible Chat API&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real-Time Updates
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Server-Sent Events (SSE)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Export
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;ZIP generation&lt;/li&gt;
&lt;li&gt;Markdown artifacts&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Why I Used Server-Sent Events Instead of WebSockets
&lt;/h1&gt;

&lt;p&gt;Many AI demos use WebSockets.&lt;/p&gt;

&lt;p&gt;For DevTeam AI, Server-Sent Events were a better fit.&lt;/p&gt;

&lt;p&gt;The application mainly streams:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;agent progress&lt;/li&gt;
&lt;li&gt;stage updates&lt;/li&gt;
&lt;li&gt;logs&lt;/li&gt;
&lt;li&gt;completion events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Communication is almost entirely server → client.&lt;/p&gt;

&lt;p&gt;SSE provided:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;simpler implementation&lt;/li&gt;
&lt;li&gt;automatic reconnection&lt;/li&gt;
&lt;li&gt;lower overhead&lt;/li&gt;
&lt;li&gt;easier deployment behind Nginx&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes the simpler solution is the better engineering choice.&lt;/p&gt;




&lt;h1&gt;
  
  
  One Feature I'm Particularly Proud Of
&lt;/h1&gt;

&lt;p&gt;One feature I added late in development dramatically improved the user experience.&lt;/p&gt;

&lt;p&gt;As more workflow stages were added, the interface became overwhelming.&lt;/p&gt;

&lt;p&gt;I redesigned the workspace so that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;only the active workflow expands automatically&lt;/li&gt;
&lt;li&gt;completed stages collapse&lt;/li&gt;
&lt;li&gt;future stages remain collapsed&lt;/li&gt;
&lt;li&gt;users can manually expand any stage when needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This small UX improvement makes navigating long AI workflows significantly easier.&lt;/p&gt;

&lt;p&gt;It reinforces the idea that good AI products are about more than model quality—they're also about thoughtful interaction design.&lt;/p&gt;




&lt;h1&gt;
  
  
  Challenges I Faced
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Prompt Isolation
&lt;/h2&gt;

&lt;p&gt;Early versions produced nearly identical responses.&lt;/p&gt;

&lt;p&gt;The Product Manager sounded like the Architect.&lt;/p&gt;

&lt;p&gt;The Backend Agent sounded like the QA Agent.&lt;/p&gt;

&lt;p&gt;The solution wasn't a better model.&lt;/p&gt;

&lt;p&gt;It was better prompt engineering.&lt;/p&gt;

&lt;p&gt;Each agent was redesigned to produce unique artifacts with clear responsibilities.&lt;/p&gt;




&lt;h2&gt;
  
  
  State Management
&lt;/h2&gt;

&lt;p&gt;Unlike a chatbot, DevTeam AI maintains project state across many stages.&lt;/p&gt;

&lt;p&gt;The application tracks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;approvals&lt;/li&gt;
&lt;li&gt;revisions&lt;/li&gt;
&lt;li&gt;dependencies&lt;/li&gt;
&lt;li&gt;generated artifacts&lt;/li&gt;
&lt;li&gt;conflicts&lt;/li&gt;
&lt;li&gt;baseline comparisons&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Managing this workflow became one of the most interesting engineering problems in the project.&lt;/p&gt;




&lt;h2&gt;
  
  
  Designing Productive Disagreements
&lt;/h2&gt;

&lt;p&gt;Perhaps the biggest lesson was this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adding more AI agents doesn't automatically improve quality.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Agents need meaningful reasons to interact.&lt;/p&gt;

&lt;p&gt;That's why the CTO Reviewer and Negotiator exist.&lt;/p&gt;

&lt;p&gt;Without review and disagreement, multi-agent systems quickly become multiple copies of the same assistant.&lt;/p&gt;




&lt;h1&gt;
  
  
  What the Platform Generates
&lt;/h1&gt;

&lt;p&gt;From a single product idea, DevTeam AI can produce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Product Requirements Document&lt;/li&gt;
&lt;li&gt;Architecture documentation&lt;/li&gt;
&lt;li&gt;Mermaid diagrams&lt;/li&gt;
&lt;li&gt;API specifications&lt;/li&gt;
&lt;li&gt;Database schema&lt;/li&gt;
&lt;li&gt;Frontend implementation plan&lt;/li&gt;
&lt;li&gt;QA strategy&lt;/li&gt;
&lt;li&gt;CTO review report&lt;/li&gt;
&lt;li&gt;Negotiation decisions&lt;/li&gt;
&lt;li&gt;Revision summaries&lt;/li&gt;
&lt;li&gt;Starter project&lt;/li&gt;
&lt;li&gt;Code review&lt;/li&gt;
&lt;li&gt;Exportable ZIP package&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Lessons Learned
&lt;/h1&gt;

&lt;p&gt;This project changed how I think about AI systems.&lt;/p&gt;

&lt;p&gt;The future isn't simply making one model smarter.&lt;/p&gt;

&lt;p&gt;It's enabling multiple specialized agents to collaborate effectively while keeping humans firmly in control.&lt;/p&gt;

&lt;p&gt;The most valuable ideas weren't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;adding more prompts&lt;/li&gt;
&lt;li&gt;adding more agents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead, they were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;clearer responsibilities&lt;/li&gt;
&lt;li&gt;approval workflows&lt;/li&gt;
&lt;li&gt;review cycles&lt;/li&gt;
&lt;li&gt;negotiation&lt;/li&gt;
&lt;li&gt;structured artifacts&lt;/li&gt;
&lt;li&gt;transparency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The quality of collaboration mattered far more than the number of agents.&lt;/p&gt;




&lt;h1&gt;
  
  
  What's Next
&lt;/h1&gt;

&lt;p&gt;Although this was built for the hackathon, I see plenty of room for future improvements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Persistent long-term memory using vector databases&lt;/li&gt;
&lt;li&gt;Retrieval-Augmented Generation (RAG) for organizational knowledge&lt;/li&gt;
&lt;li&gt;Support for additional programming languages and frameworks&lt;/li&gt;
&lt;li&gt;CI/CD planning agents&lt;/li&gt;
&lt;li&gt;DevOps and infrastructure review agents&lt;/li&gt;
&lt;li&gt;Security and compliance agents&lt;/li&gt;
&lt;li&gt;Team collaboration with shared workspaces&lt;/li&gt;
&lt;li&gt;Deployment automation&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;Building DevTeam AI for the QwenCloud Global AI Hackathon was more than an opportunity to experiment with multi-agent AI—it was a chance to rethink how software teams can collaborate with intelligent systems.&lt;/p&gt;

&lt;p&gt;QwenCloud provided the reasoning engine behind every specialized agent, while FastAPI, Next.js, and a workflow-driven architecture transformed those individual conversations into a coordinated software delivery process.&lt;/p&gt;

&lt;p&gt;Rather than replacing software engineers, DevTeam AI demonstrates how AI can augment each stage of product development: from refining ideas and reviewing architectures to negotiating trade-offs and generating implementation scaffolds. The result isn't a single AI assistant—it is an AI engineering team that collaborates, critiques, revises, and works alongside humans.&lt;/p&gt;

&lt;p&gt;As AI continues to evolve, I believe the next generation of developer tools won't revolve around isolated prompts. They will revolve around intelligent, specialized agents working together through structured workflows, with humans guiding the decisions that matter most.&lt;/p&gt;

&lt;p&gt;DevTeam AI is my exploration of that future—and QwenCloud made it possible to bring that vision to life during this hackathon.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; DevTeam AI&lt;br&gt;
&lt;strong&gt;Built for:&lt;/strong&gt; QwenCloud Global AI Hackathon – Agent Society Track&lt;br&gt;
&lt;strong&gt;Tech Stack:&lt;/strong&gt; QwenCloud, FastAPI, Python, Next.js, TypeScript, Tailwind CSS, SQLite, Server-Sent Events&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>webdev</category>
      <category>alibabachallenge</category>
    </item>
    <item>
      <title>Protect Your Node.js API: Rate Limiting with Fixed Window, Sliding Window, and Token Bucket</title>
      <dc:creator>Odunayo Dada</dc:creator>
      <pubDate>Wed, 10 Sep 2025 09:50:49 +0000</pubDate>
      <link>https://dev.to/odunayo_dada/protect-your-nodejs-api-rate-limiting-with-fixed-window-sliding-window-and-token-bucket-4278</link>
      <guid>https://dev.to/odunayo_dada/protect-your-nodejs-api-rate-limiting-with-fixed-window-sliding-window-and-token-bucket-4278</guid>
      <description>&lt;p&gt;Rate limiting is a strategy for limiting the number of requests a client or user can make to a network, application or API within a specified time (per minute, per second).&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is Rate Limiting Important?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Protects Resources from Misuse&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Without rate limiting, a single client (or bot) would be able to make thousands of requests within seconds. This can crash your server, increase expense (if you pay per API call or compute time), and reduce performance for every other user. With rate limiting, you block any single client from taking over your system’s resources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Stops Denial-of-Service (DoS) Attacks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Attackers will normally try to flood servers with traffic in an effort to make the service unavailable. Rate limiting counteracts the impact of such an attack by turning off abusive requests before they consume all of your bandwidth, memory, or CPU. While it’s not a complete solution against volumetric Distributed DoS (DDoS), it’s an essential first line of defense.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Enforces Fair Use&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In multi-user systems or public APIs, you want everyone to have a fair share of access. Rate limiting ensures that one client doesn’t hog the service at the expense of others. For example, in an API that allows 100 requests per minute, every user gets the same opportunity, preventing abuse and maintaining a consistent experience across the board.&lt;/p&gt;

&lt;p&gt;In this article, we’ll look at three (3) popular approaches for implementing rate limiting.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Fixed window&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sliding window&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Token bucket / Leaky bucket&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;1. Fixed Window&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Fixed Window strategy counts requests within a strict time window (like seconds, minutes or hour). When the window resets, the count resets too.&lt;/p&gt;

&lt;p&gt;For example, if an API request is limited to 5 requests per minute. The API cannot take more than 5 requests in every minutes and this threshold resets every 1 minute.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Request, Response, NextFunction } from 'express';

// Configuration
const WINDOW_SIZE_IN_MS = 60_000; // 1 minute
const MAX_REQUESTS = 5; // per IP per window

// Store: { ip -&amp;gt; { count, windowStart } }
type Entry = { count: number; windowStart: number };
const store = new Map&amp;lt;string, Entry&amp;gt;();

export function fixedWindowLimiter(req: Request, res: Response, next: NextFunction) {
  const ip = req.ip || req.connection.remoteAddress || 'unknown';
  const now = Date.now();

  let entry = store.get(ip);

  if (!entry) {
    // First request for this IP
    store.set(ip, { count: 1, windowStart: now });
    return next();
  }

  // If current window expired → reset counter
  if (now - entry.windowStart &amp;gt;= WINDOW_SIZE_IN_MS) {
    entry = { count: 1, windowStart: now };
    store.set(ip, entry);
    return next();
  }

  // Still inside the window
  entry.count++;

  if (entry.count &amp;gt; MAX_REQUESTS) {
    const retryAfter = Math.ceil((entry.windowStart + WINDOW_SIZE_IN_MS - now) / 1000);

    res.setHeader('Retry-After', retryAfter.toString());
    return res.status(429).json({
      success: false,
      message: `Too many requests. Try again in ${retryAfter}s`
    });
  }

  return next();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Sliding window&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Unlike fixed window rate limiting (which resets counters at regular intervals), the sliding window method continuously evaluates requests based on a moving time window.&lt;/p&gt;

&lt;p&gt;For example, an API with a limit of 100 requests per minute:&lt;/p&gt;

&lt;p&gt;If a user sends 90 requests in the last 50 seconds, they can only send 10 more in the next 10 seconds.&lt;/p&gt;

&lt;p&gt;Every second, the window slides forward, dropping old requests and including new ones.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Request, Response, NextFunction } from 'express';
import Redis from 'ioredis';

const redis = new Redis();
const WINDOW_SIZE = 60; // seconds
const MAX_REQUESTS = 100;

export async function slidingWindowLimiter(req: Request, res: Response, next: NextFunction) {
  const key = `sliding:${req.ip}`;
  const now = Date.now();

  const windowStart = now - WINDOW_SIZE * 1000;

  // Remove old requests outside window
  await redis.zremrangebyscore(key, 0, windowStart);

  // Count requests in window
  const count = await redis.zcard(key);

  if (count &amp;gt;= MAX_REQUESTS) {
    res.setHeader('Retry-After', String(WINDOW_SIZE));
    return res.status(429).json({ message: 'Too many requests' });
  }

  // Add current request timestamp
  await redis.zadd(key, now, now.toString());
  await redis.expire(key, WINDOW_SIZE);

  next();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Token bucket / Leaky bucket&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Allows bursts up to a capacity; then refills gradually. It enforces a strict, constant rate of processing, smoothing out traffic.&lt;/p&gt;

&lt;p&gt;Here is how it works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Requests are added to a queue (the bucket).&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The bucket leaks at a fixed rate (requests are processed one at a time at. regular intervals).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If the bucket overflows (too many requests), excess requests are dropped.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Request, Response, NextFunction } from 'express';
import { RateLimiterRedis } from 'rate-limiter-flexible';
import Redis from 'ioredis';

const redis = new Redis();

const limiter = new RateLimiterRedis({
  storeClient: redis,
  keyPrefix: 'bucket',
  points: 150, // bucket capacity
  duration: 60, // refill window (60s → 150 tokens per minute)
  execEvenly: true // smooth out evenly
});

export async function tokenBucketLimiter(req: Request, res: Response, next: NextFunction) {
  try {
    await limiter.consume(req.ip, 1);
    next();
  } catch (rejRes) {
    const retrySecs = Math.ceil(rejRes.msBeforeNext / 1000) || 1;
    res.set('Retry-After', String(retrySecs));
    res.status(429).json({ message: 'Too many requests, retry later' });
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rate limiting is one of the simplest yet most effective tools for protecting your APIs. Whether you choose fixed window, sliding window or token bucket, the right strategy depends on your app’s traffic patterns and scaling needs.&lt;/p&gt;

&lt;p&gt;Fixed Window :easy to implement, good for small projects.&lt;br&gt;
Sliding Window: fairer distribution, great for APIs with steady load.&lt;br&gt;
Token Bucket / Leaky Bucket: best for production, balances bursts and consistency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Rate limiting is more than just a performance trick — it’s a safeguard against abuse, downtime, and unfair usage. We’ve explored three powerful strategies: Fixed Window, Sliding Window, and Token/Leaky Bucket.&lt;/p&gt;

&lt;p&gt;But this is just the beginning. In my YouTube video, I’ll show you how to set up real rate limiting in a Node.js app, step by step, with live coding examples and best practices to keep your APIs secure and efficient.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://www.youtube.com/watch?v=kqVFuG_zbEM" rel="noopener noreferrer"&gt;Watch the full video here&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>backend</category>
      <category>api</category>
      <category>node</category>
    </item>
    <item>
      <title>Offline-First Mobile App Architecture: Syncing, Caching, and Conflict Resolution</title>
      <dc:creator>Odunayo Dada</dc:creator>
      <pubDate>Wed, 16 Jul 2025 14:46:39 +0000</pubDate>
      <link>https://dev.to/odunayo_dada/offline-first-mobile-app-architecture-syncing-caching-and-conflict-resolution-1j58</link>
      <guid>https://dev.to/odunayo_dada/offline-first-mobile-app-architecture-syncing-caching-and-conflict-resolution-1j58</guid>
      <description>&lt;p&gt;In many parts of the world, network connectivity is unreliable. Even in major cities, mobile users frequently lose signal while commuting, entering buildings, or during power outages.&lt;/p&gt;

&lt;p&gt;If your app stops working the moment internet access is lost, you’re building for ideal conditions , not the real world.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffa5bg5lttmbvntlkbuus.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffa5bg5lttmbvntlkbuus.jpg" alt="Mobile App with no network" width="720" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Offline-First Design&lt;/strong&gt;&lt;br&gt;
An offline-first application is designed to work seamlessly with or without the internet. It uses local storage as the primary data source and synchronizes changes with a remote server once connectivity is available.&lt;/p&gt;

&lt;p&gt;This approach ensures your users can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Continue working uninterrupted&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Avoid data loss&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Trust your app to be available at all times&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Real-World Use Case:&lt;/strong&gt; Field Data Collection in Rural Areas&lt;br&gt;
I will share my approach to building a field data collection app for areas with poor internet. I had to ensure that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User input was never lost&lt;/li&gt;
&lt;li&gt;Data could be submitted at any time — whether online or not&lt;/li&gt;
&lt;li&gt;The app could sync data automatically once network resumed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s how I implemented this using Room, WorkManager, and NetworkCallback in Android.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Persisting Data Locally with Room&lt;/strong&gt;&lt;br&gt;
Whenever a user captures data (e.g., survey responses or inspection records), it is saved in a local SQLite database using Room.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Entity(tableName = "field_data")
data class FieldDataEntity(
    @PrimaryKey val id: String = UUID.randomUUID().toString(),
    val formName: String,
    val content: String,
    val isSynced: Boolean = false,
    val timestamp: Long = System.currentTimeMillis()
)

@Dao
interface FieldDataDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(fieldData: FieldDataEntity)

    @Query("SELECT * FROM field_data WHERE isSynced = 0")
    suspend fun getUnsyncedData(): List&amp;lt;FieldDataEntity&amp;gt;

    @Query("UPDATE field_data SET isSynced = 1 WHERE id = :id")
    suspend fun markAsSynced(id: String)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Syncing with the Server Using WorkManager:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once the device regains network access, a background worker is triggered to sync all unsynced data in the room database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class DataSyncWorker(
    context: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {

    private val dao = AppDatabase.getInstance(context).fieldDataDao()
    private val api = ApiService.create() 

    override suspend fun doWork(): Result {
        val unsynced = dao.getUnsyncedData()
        for (item in unsynced) {
            try {
                val response = api.uploadFieldData(item)
                if (response.isSuccessful) {
                    dao.markAsSynced(item.id)
                    sendInAppNotification("Data synced: ${item.formName}")
                }
            } catch (e: Exception) {
                // Retry later
            }
        }
        return Result.success()
    }

    private fun sendInAppNotification(message: String) {
        // Optionally notify user
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Detecting Network Availability&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Used Connectivity Manager to detect when the device regains internet access and enqueue the sync worker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fun registerNetworkCallback(context: Context) {
    val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    val request = NetworkRequest.Builder().addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET).build()

    connectivityManager.registerNetworkCallback(request, object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) {
            enqueueSyncWorker(context)
        }
    })
}

fun enqueueSyncWorker(context: Context) {
    val request = OneTimeWorkRequestBuilder&amp;lt;DataSyncWorker&amp;gt;()
        .setConstraints(Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build())
        .build()

    WorkManager.getInstance(context).enqueue(request)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Conflict Resolution Strategy&lt;/strong&gt;&lt;br&gt;
In my case, conflicts were minimal because users didn’t edit the same record from multiple devices. However, in more collaborative apps, you can handle conflicts by:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Last-write-wins:&lt;/strong&gt; simplest, but risky&lt;br&gt;
Merge strategies: combine changes from client + server&lt;br&gt;
User-assisted: notify user to choose the correct version&lt;br&gt;
User Feedback: In-App and Push Notifications&lt;/p&gt;

&lt;p&gt;Once sync completes, we notify the user via:&lt;/p&gt;

&lt;p&gt;In-app snackbar/toast (if app is foregrounded)&lt;br&gt;
Push notification using a local notification&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Lessons:&lt;/strong&gt;&lt;br&gt;
Prioritize local-first design when working in regions with poor internet&lt;br&gt;
Always queue unsynced data instead of blocking the user&lt;br&gt;
Use WorkManager + Room + NetworkCallback for resilient, testable sync logic.&lt;br&gt;
Don’t forget about conflict resolution — design for edge cases&lt;/p&gt;

&lt;p&gt;Have you built offline-first apps before? I’d love to hear how you approached syncing and caching in the comments.&lt;/p&gt;

</description>
      <category>offline</category>
      <category>design</category>
      <category>android</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Offline-First Mobile App Architecture: Syncing, Caching, and Conflict Resolution</title>
      <dc:creator>Odunayo Dada</dc:creator>
      <pubDate>Wed, 16 Jul 2025 14:46:39 +0000</pubDate>
      <link>https://dev.to/odunayo_dada/offline-first-mobile-app-architecture-syncing-caching-and-conflict-resolution-518n</link>
      <guid>https://dev.to/odunayo_dada/offline-first-mobile-app-architecture-syncing-caching-and-conflict-resolution-518n</guid>
      <description>&lt;p&gt;In many parts of the world, network connectivity is unreliable. Even in major cities, mobile users frequently lose signal while commuting, entering buildings, or during power outages.&lt;/p&gt;

&lt;p&gt;If your app stops working the moment internet access is lost, you’re building for ideal conditions , not the real world.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffa5bg5lttmbvntlkbuus.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffa5bg5lttmbvntlkbuus.jpg" alt="Mobile App with no network" width="720" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Offline-First Design&lt;/strong&gt;&lt;br&gt;
An offline-first application is designed to work seamlessly with or without the internet. It uses local storage as the primary data source and synchronizes changes with a remote server once connectivity is available.&lt;/p&gt;

&lt;p&gt;This approach ensures your users can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Continue working uninterrupted&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Avoid data loss&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Trust your app to be available at all times&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Real-World Use Case:&lt;/strong&gt; Field Data Collection in Rural Areas&lt;br&gt;
I will share my approach to building a field data collection app for areas with poor internet. I had to ensure that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User input was never lost&lt;/li&gt;
&lt;li&gt;Data could be submitted at any time — whether online or not&lt;/li&gt;
&lt;li&gt;The app could sync data automatically once network resumed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here’s how I implemented this using Room, WorkManager, and NetworkCallback in Android.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Persisting Data Locally with Room&lt;/strong&gt;&lt;br&gt;
Whenever a user captures data (e.g., survey responses or inspection records), it is saved in a local SQLite database using Room.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Entity(tableName = "field_data")
data class FieldDataEntity(
    @PrimaryKey val id: String = UUID.randomUUID().toString(),
    val formName: String,
    val content: String,
    val isSynced: Boolean = false,
    val timestamp: Long = System.currentTimeMillis()
)

@Dao
interface FieldDataDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(fieldData: FieldDataEntity)

    @Query("SELECT * FROM field_data WHERE isSynced = 0")
    suspend fun getUnsyncedData(): List&amp;lt;FieldDataEntity&amp;gt;

    @Query("UPDATE field_data SET isSynced = 1 WHERE id = :id")
    suspend fun markAsSynced(id: String)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Syncing with the Server Using WorkManager:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once the device regains network access, a background worker is triggered to sync all unsynced data in the room database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;class DataSyncWorker(
    context: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {

    private val dao = AppDatabase.getInstance(context).fieldDataDao()
    private val api = ApiService.create() 

    override suspend fun doWork(): Result {
        val unsynced = dao.getUnsyncedData()
        for (item in unsynced) {
            try {
                val response = api.uploadFieldData(item)
                if (response.isSuccessful) {
                    dao.markAsSynced(item.id)
                    sendInAppNotification("Data synced: ${item.formName}")
                }
            } catch (e: Exception) {
                // Retry later
            }
        }
        return Result.success()
    }

    private fun sendInAppNotification(message: String) {
        // Optionally notify user
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Detecting Network Availability&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Used Connectivity Manager to detect when the device regains internet access and enqueue the sync worker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fun registerNetworkCallback(context: Context) {
    val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    val request = NetworkRequest.Builder().addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET).build()

    connectivityManager.registerNetworkCallback(request, object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) {
            enqueueSyncWorker(context)
        }
    })
}

fun enqueueSyncWorker(context: Context) {
    val request = OneTimeWorkRequestBuilder&amp;lt;DataSyncWorker&amp;gt;()
        .setConstraints(Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build())
        .build()

    WorkManager.getInstance(context).enqueue(request)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Conflict Resolution Strategy&lt;/strong&gt;&lt;br&gt;
In my case, conflicts were minimal because users didn’t edit the same record from multiple devices. However, in more collaborative apps, you can handle conflicts by:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Last-write-wins:&lt;/strong&gt; simplest, but risky&lt;br&gt;
Merge strategies: combine changes from client + server&lt;br&gt;
User-assisted: notify user to choose the correct version&lt;br&gt;
User Feedback: In-App and Push Notifications&lt;/p&gt;

&lt;p&gt;Once sync completes, we notify the user via:&lt;/p&gt;

&lt;p&gt;In-app snackbar/toast (if app is foregrounded)&lt;br&gt;
Push notification using a local notification&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Lessons:&lt;/strong&gt;&lt;br&gt;
Prioritize local-first design when working in regions with poor internet&lt;br&gt;
Always queue unsynced data instead of blocking the user&lt;br&gt;
Use WorkManager + Room + NetworkCallback for resilient, testable sync logic.&lt;br&gt;
Don’t forget about conflict resolution — design for edge cases&lt;/p&gt;

&lt;p&gt;Have you built offline-first apps before? I’d love to hear how you approached syncing and caching in the comments.&lt;/p&gt;

</description>
      <category>offline</category>
      <category>design</category>
      <category>android</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
