Partition Labels – Greedy + Two Pointer Thinking

This problem looks confusing at first…
“Split the string so each character appears in only one part” . But once you see the pattern, it becomes very clean.

Key Idea

Each character has a last occurrence in the string.
-> If you know how far a character goes,
you can decide where to cut the partition

Approach (Simple Thinking)

Step 1: Store last position of each character

last[s[i] - 'a'] = i;

Now you know:
-> where each character must end

Step 2: Traverse and expand partition

Start from index 0
Keep updating end = farthest last occurrence seen so far
end = max(end, last[s[i] - 'a']);
Step 3: When i == end

You found a valid partition store its length start a new partition

Code (C++)

class Solution {
public:
    vector<int> partitionLabels(string s) {
        vector<int> last(26);

        //step 1:store last index
        for(int i = 0; i < s.size(); i++) {
            last[s[i] - 'a'] = i;
        }

        vector<int> res;
        int start = 0, end = 0;

        //step 2:expand window
        for(int i = 0; i < s.size(); i++) {
            end = max(end, last[s[i] - 'a']);

            //step 3:cut partition
            if(i == end) {
                res.push_back(end - start + 1);
                start = i + 1;
            }
        }

        return res;
    }
};

Example

Input: -> “ababcbacadefegdehijhklij”

Output: -> [9, 7, 8]

How to Think

  1. Treat it like a window
  2. Expand until all characters are “safe”
  3. When no future dependency -> cut

Complexity

Time: O(n)
Space: O(1) (26 characters only)

What I Learned

  • Not all problems are pure two-pointer
  • This is more like greedy + window expansion
  • Tracking last occurrence is the key trick

Building “Architects of Chaos”: A Physics-Based AI Building Competition

In this blog post, we’ll dive into the technical details of Architects of Chaos, a physics-based building game where two AI agents compete to build the tallest and most stable structure.

The Concept

The core idea was to create a game that blends physics-based simulation with strategic AI decision-making. We wanted to see how different building philosophies (stability vs. height) would fare against each other and against unpredictable environmental challenges.

The Tech Stack

  • React 18+: For the UI and state management.
  • Matter.js: A 2D physics engine for the web, handling all collisions, gravity, and constraints.
  • Tailwind CSS: For modern, responsive styling and layout.
  • Motion (Framer Motion): For smooth UI animations and transitions.
  • Lucide React: For a consistent and clean icon set.

Architecture: Syncing React and Matter.js

The biggest technical challenge was keeping the React state and the Matter.js physics engine in sync. We used a central game loop within a useEffect hook in GameCanvas.tsx.

The Game Loop

The game loop is responsible for:

  1. Updating Physics: Stepping the Matter.js engine.
  2. Calculating Metrics: Determining the height and stability of each agent’s structure.
  3. Managing Phases: Transitioning between agent turns, settling phases, and environmental events.
// Simplified game loop in GameCanvas.tsx
useEffect(() => {
  const runner = Matter.Runner.create();
  Matter.Runner.run(runner, engineRef.current);

  const updateLoop = setInterval(() => {
    if (gameState.isPaused) return;

    // Calculate metrics
    updateAgentMetrics(agentA, setAgentA);
    updateAgentMetrics(agentB, setAgentB);

    // Phase management
    if (gameState.phase === 'SETTLING') {
      // Check if all bodies have settled
      const allSettled = bodies.every(b => b.speed < 0.1);
      if (allSettled) {
        // Transition to next phase
      }
    }
  }, 100);

  return () => {
    Matter.Runner.stop(runner);
    clearInterval(updateLoop);
  };
}, [gameState.isPaused, gameState.phase]);

Real-Time Stability Metrics

One of the most engaging features of the game is the Real-Time Stability Gauge. Unlike simple height trackers, this gauge calculates structural integrity on the fly.

We factor in several physical properties:

  1. Center of Mass Offset: How far the structure’s mass is from its base center.
  2. Structural Leaning: The horizontal deviation of the tower.
  3. Real-Time Velocity: If blocks are still moving or vibrating, stability drops.
  4. Aspect Ratio: A wide “pile” is naturally more stable than a thin “tower,” and the gauge reflects this architectural reality.

When stability drops below 30%, the gauge enters a CRITICAL state, pulsing red to warn the player of an imminent collapse.

AI Agents: Strategy vs. Ambition

The AI agents, Fortress Architect and High-Rise Maverick, have distinct building strategies implemented in src/logic/ai.ts.

  • Fortress Architect: Prioritizes a wide base and stability.
  • High-Rise Maverick: Focuses on stacking blocks vertically for maximum height.
// Example AI logic for Fortress Architect
export function getAgentAMove(agentBodies: Matter.Body[], plotRange: [number, number], round: number, difficulty: Difficulty): BlockPlacement {
  // ... logic to find a stable spot near the center of existing mass ...
  const avgX = agentBodies.reduce((acc, b) => acc + b.position.x, 0) / agentBodies.length;
  let x = avgX + (Math.random() - 0.5) * jitter;

  // Ensure the block stays within the plot boundaries
  x = Math.max(plotRange[0] + halfWidth, Math.min(plotRange[1] - halfWidth, x));

  return { type, x, rotation };
}

🌪️ Environmental Challenges: Adding Chaos

To make the game more dynamic, we introduced environmental challenges like Slippery Ground, Earthquakes, and The Storm. These effects are triggered randomly and their intensity scales with the selected difficulty.

  • Slippery Ground: Temporarily reduces friction on all blocks.
  • Earthquake: Applies a brief vertical force to all blocks.
  • The Storm: A final, high-intensity phase where gravity and forces are randomized.

Architects of Chaos demonstrates how physics-based simulations can be integrated into a React application to create engaging and unpredictable gameplay. By combining strategic AI with dynamic environmental effects, we’ve created a game that is both fun to play and technically interesting to build.

Check out the full source code on GitHub.

From Spaghetti to Structure: Why I Migrated My Production Flutter App to Clean Architecture

Flutter clean architecture diagram

“First make it work, then make it beautiful and robust.”

I’m a junior dev. I built a real app — Anahad — that real people use. And for a while, I was proud of it.

Then it grew. And I wasn’t so proud anymore.

This isn’t a textbook post about Clean Architecture. This is the story of what broke, why I finally made the switch, and what actually clicked for me — told by someone who was in the trenches with a messy codebase and had to ship features at the same time.

The Mess I Was Living In

Picture this: 6 features. Each with their own screens, widgets, blocs, services, and utils — all distributed across a directory structure that looked neat on day one and became a labyrinth by month three.

Most screens were monoliths — all-in-one files with UI and logic tangled together. The ones that did use components? Everything went into a global widgets/ directory. So you’d have prop drilling across three levels just to pass something that one widget needed. Tracking the flow of data meant hopping between directories, mentally stitching together a picture of how things connected.

Error handling was… creative. I’d built my own AsyncResult<Success, Error> sealed class — functional programming vibes without the actual library. It worked at the service layer, but it was an island. Nothing else was consistent.

And the worst part? My services were tightly coupled to Dio and the remote API. No abstraction. No interface. You couldn’t mock anything. You couldn’t test anything. If Dio failed, the whole thing failed, and you had to dig to figure out why.

The moment I truly felt it was when I had to trace a bug and found myself drilling through 5 directories just to understand the flow. That’s when I knew — this isn’t scaling. This is surviving.

What Is Flutter Clean Architecture, Actually?

Before I get into the migration, let me walk you through the structure — because once it clicked, I couldn’t unsee it.

Clean Architecture splits your app into three layers:
Flutter clean architecture diagram

🎨 Presentation Layer

This is everything the user sees and touches.

  • Screens — Your UI pages
  • Widgets — Reusable UI components
  • Bloc — State management; the brain between UI and business logic

The Bloc only talks to the Domain layer. It doesn’t know where data comes from. Doesn’t care.

🧠 Domain Layer

This is the pure heart of your app. No Flutter. No Dio. No Firebase. Just Dart.

  • Entities — Plain Dart classes representing your core data models
  • Use Cases — Single-responsibility classes that each do one thing (e.g., LoginUseCase, FetchUserProfileUseCase)
  • Repository (Interface) — Abstract contracts defining what data operations exist, not how they work

This layer has zero dependencies on anything external. It’s the most stable, most testable code in your app.

🗄️ Data Layer

This is where the real world connects.

  • Models — Extend your Entities; handle JSON serialization, API mapping
  • Repository (Implementation) — Implements the Domain’s repository interface
  • Data Sources — Remote (Dio, Firebase) and Local (Hive, SQLite)

The magic line? The Repository Implementation delegates data fetching to the datasource. The domain doesn’t care if it’s coming from the internet or a local cache.

Bloc → UseCase → Repository Interface
                        ↑
              Repository Implementation → Remote / Local Datasource

The Migration: Incremental, Not a Rewrite

Here’s the thing nobody tells you: you don’t have to nuke your codebase.

I had features to ship. Users on the app. I couldn’t just disappear for two weeks and “do clean architecture.” So I went incremental.

I started with the auth flow — the most self-contained, most critical part of the app. My service methods were already well-written, so the migration was mostly structural:

  1. Converted service methods into Use Cases
  2. Moved Dio calls into a RemoteDataSource
  3. Created a Repository Implementation that satisfied the Domain’s abstract interface
  4. Wired the Bloc to talk to the Use Case instead of the service directly

It wasn’t that hard, honestly. But the payoff was immediate.

Now if I want to add offline support to Anahad? I create a LocalDataSource, and the Repository Implementation switches between them. The Bloc doesn’t change. The Use Cases don’t change. The Domain doesn’t even know it happened.

That’s the beauty.

fpdart and Functional Programming in Use Cases

Coming from a Rust background, I was already comfortable with Option and Result types. I’d even hacked together my own AsyncResult<Success, Error> sealed class before this migration.

When I discovered fpdart, it was like: oh, this is just the proper version of what I was already doing.

In my Use Cases, I now use Either<Failure, Success> for all return types. Left is failure, right is success — and you chain transformations cleanly without nested try-catches or nullable hell.

// Use Case
class LoginUseCase {
  final AuthRepository repository;

  LoginUseCase(this.repository);

  Future<Either<Failure, User>> call(LoginParams params) async {
    return await repository.login(params.email, params.password);
  }
}
// In the Bloc
final result = await loginUseCase(LoginParams(email: email, password: password));

result.fold(
  (failure) => emit(AuthError(failure.message)),
  (user)    => emit(AuthSuccess(user)),
);

No try-catch spaghetti. No “did this return null or did it actually fail?” ambiguity. The types tell the story.

The “Aha” Moment

I’ll be honest — my first reaction to Clean Architecture was “this is way too much boilerplate.”

Why write a Use Case class that just calls a repository method? Why create an abstract interface when I could just use the implementation directly? Seemed like over-engineering for the sake of it.

Then I migrated the auth flow. And something just… clicked.

I was looking at my Bloc, and I realized: this Bloc has no idea where the user data comes from. It doesn’t import Dio. It doesn’t know about Firebase. It just calls loginUseCase() and reacts to the result.

Before this, I couldn’t test anything because everything was tightly coupled. The service needed Dio, Dio needed a real network, the network needed… you get it. Now, because the Repository is an abstract interface, I can mock it in tests trivially:

class MockAuthRepository extends Mock implements AuthRepository {}

That’s it. Inject it. Test your Use Cases in complete isolation.

Ambiguity went away. I know exactly what each file does, what it depends on, and what it’s responsible for. When something breaks, I know which layer to look at.

Should You Use Clean Architecture for Every App?

No. Please don’t.

If you’re building a weekend project or learning Flutter for the first time, don’t start with Clean Architecture. You’ll spend more time setting up the structure than building the thing, and you’ll learn why the architecture exists without ever feeling the pain it solves.

The pain is the teacher.

Build the app. Ship it. Let it grow. Feel the moment where you’re drilling through 5 directories to trace a bug, where you can’t mock a thing for testing, where adding one feature means touching 8 files in 6 different folders.

Then reach for Clean Architecture. Because then you’ll understand it — not just intellectually, but in your bones.

The architecture exists to solve real problems. Let the problems teach you first.

Quick Reference: The Layer Cheat Sheet

Layer Contains Depends On
Presentation Screens, Widgets, Bloc Domain only
Domain Entities, Use Cases, Repository Interface Nothing (pure Dart)
Data Models, Repository Impl, Data Sources Domain interfaces

What’s Next for Anahad

  • Migrating the remaining 5 features incrementally
  • Adding a proper logging layer
  • Writing actual unit tests now that mocking is possible
  • Maybe offline support — and thanks to clean architecture, it’ll be a datasource swap, not a rewrite

If you’re a fellow junior dev with a production app that’s starting to smell — I see you. You don’t have to have everything figured out from day one. Build it, feel the pain, then build it better.

That’s not a failure. That’s engineering.

Built with love (and a lot of refactoring) while working on Anahad.
Hit me up on Twitter/X if this resonated — always happy to talk Flutter, architecture, or why Rust spoils you for everything else.

I Built a Real-Time Voice AI Confession Guide for 1.39 Billion Catholics. Every Bug Was Invisible.

masstime

The Gap

75% of U.S. Catholics say they never go to confession or go less than once a year. Only 2% confess monthly. Those numbers come from Pew Research and CARA at Georgetown, and they’ve been trending in one direction for decades.

I built Mass Time, an iOS app with 280,000+ churches indexed, 60 prayers, and daily readings. I’ve watched the confession problem from the data side for years. People want to go. They feel anxious. They don’t know what to say. They haven’t been in so long that the guilt of not going becomes the reason they keep not going.

In 2025, the late Pope Francis declared a Jubilee Year and specifically called for increased access to the Sacrament of Reconciliation. The demand is real. The infrastructure isn’t.

So I built an AI confession guide. No AI will ever replace a priest. Only a priest can grant absolution. This is a preparation tool. You talk to your phone. It talks back. Seven languages. No keyboard. Just a conversation that helps you organize your thoughts before you walk into that booth.

It uses Amazon Nova 2 Sonic, a bi-directional speech-to-speech model, running on Amazon Bedrock AgentCore Runtime. The bugs I hit building it were the worst kind: silent, invisible, and plausible enough to make you doubt everything except the actual problem.

The Architecture

Simple on paper. Brutal in practice.

masstime

Nova 2 Sonic handles the heavy lifting. It takes raw audio in, processes speech bidirectionally, and sends audio back. No separate STT or TTS pipeline. One model, both directions, real-time.

The server is a Python 3.13 container on AgentCore Runtime. The client is native Swift with AVAudioEngine. No web views, no React Native. Seven polyglot voices: English (Matthew), French (Florian), Spanish (Carlos), Italian (Lorenzo), German (Lennart), Portuguese (Leo), and Hindi (Arjun). Users can switch languages mid-conversation and the AI follows.

Region constraint worth knowing: Nova 2 Sonic is only available in us-east-1, us-west-2, and ap-northeast-1. I learned the us-east-2 limitation the hard way when my first deployment returned a model-not-found error.

The Bug That Almost Shipped

Nova 2 Sonic never responded to speech. I tried the Strands Agents SDK. Raw API calls. Different prompts. Different voices. Nothing.

The audio RMS values from the mic looked normal. Waveforms had energy. Everything seemed fine.

One line of Swift was wrong.

// WRONG: reads pointer-to-pointer memory (garbage audio)
Data(bytes: outBuf.int16ChannelData!, count: ...)

// CORRECT: reads actual audio samples
Data(bytes: outBuf.int16ChannelData![0], count: ...)

int16ChannelData is UnsafePointer<UnsafeMutablePointer<Int16>>. A pointer to an array of channel pointers. Without [0], you’re reading the pointer addresses themselves as audio samples.

The RMS looked plausible because memory addresses happen to have high values. The model received garbage that looked like audio in every metric except the one that mattered: it wasn’t audio.

I confirmed it by injecting known-good PCM on the server side. Model responded perfectly. Fixed the iOS code. Real mic audio worked.

One missing array subscript. Hours of debugging.

Two More Invisible Bugs

The app crashed on every AI response with _outputFormat.channelCount == buffer.format.channelCount. AVAudioPlayerNode was connected with the mixer’s stereo format but I was scheduling mono buffers. Nova 2 Sonic outputs 24kHz mono PCM. The mixer expects stereo by default.

Fix: connect the player node with an explicit mono format matching the model’s output.

let playFmt = AVAudioFormat(
    commonFormat: .pcmFormatInt16,
    sampleRate: 24000,
    channels: 1,
    interleaved: true
)!
audioEngine?.connect(playerNode!, to: audioEngine!.mainMixerNode, format: playFmt)

Key detail that isn’t obvious from the docs: Nova 2 Sonic input is 16kHz 16-bit mono PCM, but output is 24kHz 16-bit mono PCM. Different sample rates in each direction.

Then the mic auto-unmute timer never fired. After the AI spoke, the mic stayed muted permanently. No error. No warning. Just silence.

Timer.scheduledTimer was called from the WebSocket callback thread. That thread has no RunLoop. The timer was created, registered to nothing, and quietly ignored. This is one of those iOS gotchas that experienced developers know and everyone else discovers at 2 AM.

The Cognito Session Policy Trap

This one is invisible and will waste your entire day.

Cognito Identity Pools have two auth flows: Enhanced (default) and Basic (classic). Every tutorial uses Enhanced. It works fine for S3, DynamoDB, Lambda.

It does not work for Amazon Bedrock (currently).

Enhanced flow calls getCredentialsForIdentity, which injects a restrictive session policy that limits credentials to a subset of AWS services. Bedrock is not in that subset. Your IAM role policy can be perfect. You’ll still get AccessDeniedException.

The error message says “no session policy allows” but doesn’t tell you Cognito is injecting it. You can’t see this policy in IAM, CloudTrail, or the Cognito console. It’s completely invisible.

Fix: one boolean in CDK.

const identityPool = new cognito.CfnIdentityPool(this, 'Pool', {
    allowUnauthenticatedIdentities: true,
    allowClassicFlow: true,  // This is the entire fix
});

On the client side, the classic flow requires three calls instead of one: getId → getOpenIdToken → STS AssumeRoleWithWebIdentity. The STS call is an unsigned HTTP POST, so you don’t even need the STS SDK.

One boolean. Hours of debugging. Not documented clearly anywhere. This affects all Bedrock actions, not just Nova 2 Sonic.

Frugal Architecture: 60 Concurrent Sessions, Zero Quota Increases

Werner Vogels talks about the Frugal Architect: cost-aware design as a first-class engineering concern, not an afterthought. This project runs on my personal AWS account. No technical account manager. No need to request quota increases through a support plan. No need for using my “AWS Hero” Card.

Nova 2 Sonic has a default service quota of 20 concurrent InvokeModelWithBidirectionalStream requests per region (check your Service Quotas console under Amazon Bedrock for the exact current value). That’s 20 simultaneous confessions per region before you hit throttling.

Instead of requesting a quota increase, I built a queue-based routing system across all three available regions.

REGIONS = ['us-east-1', 'us-west-2', 'ap-northeast-1']
MAX_PER_REGION = 20

def pick_region(counts):
    best = None
    for region in REGIONS:
        c = counts.get(region, 0)
        if c < MAX_PER_REGION and (best is None or c < counts.get(best, 0)):
            best = region
    return best

3 regions × 20 concurrent sessions = 60 simultaneous confession sessions. On default quotas. No support ticket. No TAM call.

The latency difference between regions is negligible for this use case. A confession guide isn’t a trading bot. An extra 80ms of round-trip to Tokyo doesn’t matter when the conversation has natural 2-second pauses between turns.

Design around the constraints you have instead of asking for the constraints to be removed.

Nova 2 Sonic Configuration That Matters

Turn detection sensitivity set to LOW for confession preparation. That’s roughly a 2-second pause before the model responds. You want thoughtful pauses in this context, not rapid-fire conversation.

await send_evt({"event": {"sessionStart": {
    "inferenceConfiguration": {"maxTokens": 1024, "topP": 0.9, "temperature": 0.7},
    "turnDetectionConfiguration": {"endpointingSensitivity": "LOW"}
}}})

Available values: LOW, MEDIUM, HIGH. For most conversational use cases, MEDIUM is fine. For reflective, thoughtful conversations, LOW gives the user space to think.

Connection limit is 8 minutes per WebSocket connection. For longer sessions, AWS provides a session continuation pattern in their samples.

System prompt gotchas worth knowing:

  • Without explicit instructions, the model repeats its welcome message on every turn. You need: “FIRST RESPONSE ONLY: Welcome with ‘In the name of the Father…’ AFTER THE FIRST RESPONSE: Do NOT repeat the welcome ever again.”
  • Hindi requires the speech instruction appended inline to the system prompt text, not as a separate content block. A separate block caused Hindi to be completely silent. No error. Just silence.
  • Nova 2 Sonic sends { "interrupted": true } as JSON text during barge-in. Filter it server-side or your transcript gets polluted with raw JSON.

Echo Cancellation: Still an Active Problem

The iPhone speaker plays the AI’s voice. The mic picks it up. The model hears itself and responds to its own echo. This is the hardest problem in the entire project.

Four approaches, in order of desperation:

  1. Flag-based mute when AI speaks. Problem: contentEnd means the server finished sending, not the speaker finished playing.
  2. Playback completion unmute using scheduleBuffer completion handlers. Better timing, still some leakage.
  3. Send silence while muted. Zero-filled PCM frames keep the bidirectional stream alive while preventing echo from reaching the model.
  4. Prompt-level echo awareness: “You will hear your own voice echoed back. If you hear words identical to what you just said, that is YOUR OWN ECHO. IGNORE IT.”

The official AWS Python sample never mutes the mic. Audio flows continuously in both directions. Nova 2 Sonic has built-in turn detection and handles echo internally. That works better on desktop where mic/speaker separation is cleaner. On a phone speaker, it’s a different story.

Current approach: silence-based muting + playback completion unmute + prompt-level echo awareness. Works about 90% of the time. The other 10% is still an active problem.

The Economics

Nova 2 Sonic pricing (per 1K tokens): $0.0034 speech input, $0.0136 speech output. That works out to roughly $0.02/min combined.

A 5-minute session costs about $0.10. Not $1. Not $5. Ten cents.

At $1.99 per session, after Apple’s 30% cut ($1.39 net), that’s about $1.29 profit per paid session. The margins are real.

The freemium flow: 1 free minute (configurable via DynamoDB, no app update needed), then a paywall. Pay $1.99 to continue for up to 5 minutes. A 30-second grace period after the paywall gives users time to decide. The free minute costs about $0.02 to deliver. Even if nobody converts, the free tier costs almost nothing to run.

Per Session
Nova 2 Sonic cost (5 min) ~$0.10
User pays $1.99
Apple’s 30% cut -$0.60
Net to developer $1.39
Profit ~$1.29

Session recordings are AI audio only, stored locally on the user’s device. Never on servers. The user can save and share via the standard iOS share sheet.

Total AWS infrastructure cost for the backend? Lambda, DynamoDB, API Gateway, AgentCore Runtime. On a personal account with low traffic, we’re talking single-digit dollars per month before any sessions even happen.

What’s Next: AgentCore WebRTC

On March 20, AWS announced WebRTC support for AgentCore Runtime. This is a big deal for this project.

Right now, the audio path is: iPhone → WebSocket → AgentCore → Nova 2 Sonic. WebSocket works, but it’s a text-based protocol carrying base64-encoded audio. Every audio frame gets encoded, wrapped in JSON, and decoded on the other end.

WebRTC is purpose-built for real-time media. Binary audio frames. Built-in echo cancellation at the protocol level. Adaptive bitrate. Jitter buffers. All the things I’ve been fighting to implement manually in Swift.

The migration path is straightforward since AgentCore Runtime supports both protocols on the same runtime. I can add a WebRTC endpoint alongside the existing WebSocket one and A/B test latency and echo cancellation quality on real devices.

If WebRTC’s built-in echo cancellation works well on iPhone speakers, that solves the hardest remaining problem in the entire project. That 10% failure rate on echo could drop to near zero.

What I’d Do Differently

Verify audio end-to-end on day one. Inject a known sine wave, record what the model receives, compare. Would have saved hours of headache.

Start with the classic Cognito flow. Don’t even try enhanced flow with Bedrock. It won’t work and the error messages won’t tell you why.

Build echo cancellation as a first-class feature, not an afterthought. On mobile, this is the hardest problem. Budget time for it.

Use Docker assets (ECR) for AgentCore from the start. Code assets (S3) seem simpler to package but the cold start timeout makes them impractical for anything with dependencies.

Design for multi-region from day one. The frugal routing across three regions was an afterthought that should have been the starting architecture. Default quotas are generous if you think horizontally.

The Point

1.39 billion baptized Catholics worldwide. A sacrament that many want to practice but feel unprepared for.

This isn’t about replacing priests. It’s about removing the anxiety barrier that keeps people from showing up in the first place. A 5-minute voice conversation that helps you organize your thoughts, in your own language, on your own time. For ten cents of compute.

Building this was fast. The debugging was not. Every major bug was the invisible kind: plausible RMS values hiding garbage audio, silent timers on runloop-less threads, session policies you can’t see in any console. The documentation had gaps. Echo cancellation on a phone speaker remains partially unsolved.

Most of the code was written with Kiro CLI. What would have taken weeks of back-and-forth between Swift, Python, and CDK was done in hours.

But someone will open this app before their first confession in 20 years. And they’ll feel a little less anxious walking in.

That’s worth ten cents. Now go build.

Your Best Engineers Are Tired

What looks like productivity acceleration is quietly erasing the recovery time built into every engineering day.

I started noticing it in my engineers first. In one-on-ones. In how the same conversations sounded different toward the end of a sprint. I heard it enough times across enough people that I started looking for it deliberately. Then I noticed it in myself. I’m not coding more than half my hours. My work is reviewing, deciding, evaluating … across systems, teams, timelines. And I was still running out of steam by Wednesday. We went from running a marathon to sprinting a marathon. Same course. Same distance. But you can’t sprint a marathon and finish the same.

Recovery Time Was Hidden in the Friction

Before AI, engineering had something built into it that nobody named. Recovery time.

Not blocked time. Not wasted time. Recovery time.

The build step that took four minutes. You’d flip to a browser tab, refill your coffee, let your brain sit in neutral for a moment. Not thinking about architecture. Not evaluating anything. Just waiting.

Writing boilerplate by hand was tedious. But tedious was the point. Low stakes. Low cognitive demand. Your hands typed while your mind coasted. There was no decision in it. Just the satisfying click of producing something familiar.

Refactoring a module you knew felt almost meditative. You knew the code. You knew what needed to move. The work had grooves. Your attention could settle into something easy while the harder thinking finished processing in the background.

None of this felt like rest. Even to the engineers themselves.

But it was rest. The throttle built into every engineering day.

Those pockets of low demand weren’t friction to optimize away. They were the throttle that kept a sustainable pace sustainable.

What AI Actually Replaced

AI didn’t just speed up those tasks. It replaced them. Not with the same tasks done faster … with entirely different tasks.

The build runs in 45 seconds now. Those four minutes are gone. And they’ve been replaced with four minutes of reviewing what the model generated.

The boilerplate writes itself. The time you saved is filled with evaluating whether it fits your architecture, follows your conventions, doesn’t introduce patterns that will calcify into problems eighteen months from now.

Every minute that used to be low stakes is now high stakes. Does this output pass the test. Does it pass the test and make sense. Does it make sense and not create downstream debt.

Every minute is an evaluation.

There’s no coasting anymore. There’s no meditative refactoring. There’s no four minute reset. There’s a stream of decisions, each one flowing directly into the next with no gap in between.

Engineers spent years training one cognitive muscle. The traditional approach to programming built a specific rhythm into the work … heavy judgment, then recovery. Heavy judgment, then recovery. The boring tasks were the low-intensity reps between hard sets.

Then came the mandate. Overnight, it demanded a completely different muscle. The training cycle that built the first one had no equivalent for the second. The new game started at full intensity immediately.

The Mental Model Most Leaders Are Running

If you lead engineers, you’re carrying this belief … AI saves time, so engineers should have more capacity.

It’s a reasonable belief. It just doesn’t map to what’s happening.

AI compresses effort so engineers hit cognitive walls earlier.

Compression is different from reduction. If a sprint used to contain forty hours of varied cognitive demand, AI compresses the low demand portions to nothing. The engineer doesn’t end up doing less. They end up doing all of it, in less time, with no recovery built in.

The output looks the same for a while. Maybe it looks better. The metrics move. The velocity numbers read well. I’ve written about what those numbers actually measure and what happens when leaders mistake them for health. The pattern is consistent … things look fine until they suddenly don’t.

AI compresses effort so engineers hit cognitive walls earlier.

What It Looks Like in Practice

Cognitive overload doesn’t announce itself. It doesn’t look like someone staring at a blank screen. It looks like things getting approved that shouldn’t. Pull requests that should have been flagged, that weren’t. Architectural shortcuts that nobody questioned in review, even though three people in that room would have caught the same thing six months ago.

The quality of decisions drops before the quantity of output drops.

By the time the output is visibly degraded, the problem has been compounding for weeks.

One signal worth watching … when your best engineers stop asking why in planning. When the people who used to push back on the framing of a problem start accepting the framing and jumping straight to solution. That’s not a focus improvement. That’s cognitive depletion showing up as compliance.

The engineers who are best at their jobs are also the most likely to push through the wall without telling anyone. Not because they’re hiding it. Because their mental model for feeling off is “I need to work harder to get back on top of things.” They’re trying to solve their way out of the problem that is the problem.

The Question Worth Asking

Look at your team’s calendar for tomorrow. Not the meetings … look at the time engineers spend building.

Ask one question about that time. Where are the moments when nobody is asking them to evaluate anything?

Where is the work that doesn’t require judgment to complete? Where is something low stakes, familiar, with grooves?

If you can find it … protect it. Don’t fill it with more AI generated output to review. Don’t schedule a ceremony into it. Let it be what it is.

If you can’t find it … you’re not looking at a capacity problem. You’re looking at a depletion problem. And it will surface on your team before it surfaces in your metrics.

What This Actually Requires

This isn’t an argument against AI tooling. The tooling is useful.

Nobody is built for sustained high stakes evaluation all day. There is a ceiling, and it isn’t a software limitation. What AI changes is how fast you reach it.

Remove the throttle without acknowledging what the throttle was doing … and the pace becomes the problem.

Give engineers work that costs less to evaluate. Protect the moments when judgment isn’t required. Design the pause into the day now that the workflow stopped doing it automatically.

Some of that is task design. Some of it is how you structure rituals and reviews. Some of it is recognizing that when an engineer asks for a day to work on something slower, they are not being resistant to change. They are self-regulating. Let them.

Your job as a leader is not to maximize the utilization of your engineers’ cognitive capacity. It’s to sustain it.

Your job as a leader is not to maximize the utilization of your engineers’ cognitive capacity. It’s to sustain it. If you can’t find the recovery time in your team’s day, you’re not running a productive engineering org. You’re running a depletion schedule.

One email a week from The Builder’s Leader. The frameworks, the blind spots, and the conversations most leaders avoid. Subscribe for free.

How I Built a Real-Time Stripe Webhook Anomaly Detector (Architecture + Code)

How I Built a Real-Time Stripe Webhook Anomaly Detector (Architecture + Code)

Stripe webhooks are the nervous system of any subscription business. When they work, everything is silent. When they break, you often don’t find out until a customer complains — or your MRR drops.

I found this out the hard way. A silent charge failure cascade wiped out $800 in MRR before I noticed. So I built a real-time webhook monitor. Here’s the architecture and the key implementation patterns.

The Core Problem With Webhook Monitoring

Stripe sends events. You receive them. But “receiving” and “processing correctly” are different things. Common failure modes:

  1. Duplicate delivery — Stripe retries events aggressively. If your handler isn’t idempotent, you get double-charges.
  2. Silent lapses — Payment fails, subscriber stays active, you don’t notice for days.
  3. Webhook lag — Events pile up because your endpoint is slow or overloaded.
  4. Fraud spikes — Sudden surge in charge.failed events from card testing attacks.
  5. Negative invoice anomalies — Credits or refunds creating unexpected negative charges.

A Stripe dashboard doesn’t catch these automatically. You need a monitor that watches the stream and fires when something looks wrong.

Architecture: Event Stream → Detectors → Alerts

Stripe Webhook Events
        ↓
Webhook Receiver (FastAPI)
        ↓
Event Store (SQLite / Postgres)
        ↓
Detector Engine (runs on every event)
        ↓
Alert Router (email / webhook / Slack)

The key design choice: run detectors on every event, not on a schedule. Scheduled checks mean you might miss a 5-minute fraud spike. Event-triggered detection means you catch it within seconds.

The Webhook Receiver

from fastapi import FastAPI, Request, HTTPException
import stripe
import hmac
import hashlib

app = FastAPI()

@app.post("/webhooks/stripe")
async def stripe_webhook(request: Request):
    payload = await request.body()
    sig_header = request.headers.get("stripe-signature")

    try:
        event = stripe.Webhook.construct_event(
            payload, sig_header, STRIPE_WEBHOOK_SECRET
        )
    except ValueError:
        raise HTTPException(status_code=400, detail="Invalid payload")
    except stripe.error.SignatureVerificationError:
        raise HTTPException(status_code=400, detail="Invalid signature")

    # Store + dispatch
    await store_event(event)
    await run_detectors(event)

    return {"status": "ok"}

Always verify the signature. Always return 200 before doing expensive work — Stripe retries if you take too long.

The Detector Pattern

Each detector is a function that takes an event and returns an alert or None:

from dataclasses import dataclass
from typing import Optional
from datetime import datetime, timedelta

@dataclass
class Alert:
    severity: str  # "low", "medium", "high", "critical"
    detector: str
    message: str
    event_id: str

async def detect_charge_failure_rate(event: dict, db) -> Optional[Alert]:
    """Alert if charge failure rate exceeds threshold in the last hour."""
    if event["type"] not in ("charge.failed", "charge.succeeded"):
        return None

    one_hour_ago = datetime.utcnow() - timedelta(hours=1)

    recent = await db.fetch_events_since(one_hour_ago, 
                                          types=["charge.failed", "charge.succeeded"])

    if len(recent) < 10:  # Not enough data
        return None

    failures = [e for e in recent if e["type"] == "charge.failed"]
    failure_rate = len(failures) / len(recent)

    if failure_rate > 0.15:  # 15% threshold
        return Alert(
            severity="high",
            detector="charge_failure_rate",
            message=f"Charge failure rate: {failure_rate:.0%} in last hour ({len(failures)}/{len(recent)} failed)",
            event_id=event["id"]
        )
    return None

Wire up multiple detectors:

DETECTORS = [
    detect_charge_failure_rate,
    detect_duplicate_events,
    detect_silent_lapse,
    detect_webhook_lag,
    detect_fraud_spike,
    detect_negative_invoice,
    detect_revenue_drop,
]

async def run_detectors(event: dict):
    for detector in DETECTORS:
        alert = await detector(event, db)
        if alert:
            await route_alert(alert)

The Idempotency Guard (Critical)

Stripe retries. Your handlers must be idempotent:

async def store_event(event: dict) -> bool:
    """Returns False if event was already processed (skip duplicate work)."""
    event_id = event["id"]

    existing = await db.fetch_one(
        "SELECT id FROM stripe_events WHERE event_id = ?", 
        (event_id,)
    )

    if existing:
        return False  # Already processed — do nothing

    await db.execute(
        "INSERT INTO stripe_events (event_id, type, created, payload) VALUES (?, ?, ?, ?)",
        (event_id, event["type"], event["created"], json.dumps(event))
    )
    return True

This prevents duplicate alerts from Stripe’s retry logic.

Silent Lapse Detection

The sneaky one. Payment fails → Stripe retries 3 times over 7 days → subscription cancels → you never noticed because the customer was “still active”:

async def detect_silent_lapse(event: dict, db) -> Optional[Alert]:
    """Detect when a subscription cancels after multiple payment failures."""
    if event["type"] != "customer.subscription.deleted":
        return None

    sub = event["data"]["object"]
    customer_id = sub["customer"]

    # Check if this customer had recent payment failures
    recent_failures = await db.fetch_events(
        customer_id=customer_id,
        types=["invoice.payment_failed"],
        since_days=10
    )

    if len(recent_failures) >= 2:
        mrr_lost = sub.get("items", {}).get("data", [{}])[0].get("price", {}).get("unit_amount", 0) / 100
        return Alert(
            severity="high",
            detector="silent_lapse",
            message=f"Silent lapse detected: customer {customer_id} cancelled after {len(recent_failures)} payment failures. MRR lost: ${mrr_lost:.2f}/mo",
            event_id=event["id"]
        )
    return None

The Metrics Endpoint

Real-time visibility into detector status:

@app.get("/metrics/detectors")
async def detector_metrics():
    last_24h = datetime.utcnow() - timedelta(hours=24)

    alerts = await db.fetch_alerts_since(last_24h)
    events = await db.fetch_events_since(last_24h)

    detector_counts = {}
    for alert in alerts:
        detector_counts[alert["detector"]] = detector_counts.get(alert["detector"], 0) + 1

    return {
        "events_processed_24h": len(events),
        "alerts_fired_24h": len(alerts),
        "active_detectors": len(DETECTORS),
        "detector_breakdown": detector_counts,
        "last_event_at": events[-1]["created"] if events else None
    }

What I Learned

  1. Start with 3 detectors, not 7. The failure rate, silent lapse, and duplicate event detectors catch 90% of real incidents. Add more later.

  2. Tune your thresholds per traffic volume. A 15% failure rate threshold makes sense at 100 charges/hour. At 10 charges/hour, it fires too often.

  3. Alert fatigue is real. Start with high thresholds, tighten as you learn what’s normal for your business.

  4. The webhook lag detector is underrated. Stripe’s webhook delivery has SLAs. If you’re seeing 10+ minute delays, something is wrong with your infrastructure.

  5. Store everything. Raw event storage is cheap. You’ll need it for debugging at 2 AM.

The Open-Source Version

I packaged this pattern as BillingWatch — self-hosted, MIT licensed. 7 detectors out of the box, real-time metrics endpoint, configurable alert thresholds.

If you’re running subscriptions on Stripe and want coverage against the failure modes above: github.com/rmbell09-lang/billingwatch

What webhook failure modes have you hit in production? Always curious what I’m missing.

Stop Missing Vinted Deals

If you spend any amount of time hunting for specific items online, you know the frustration of opening an app only to see that the exact jacket or camera lens you wanted sold three minutes ago. I recently decided I wanted to never miss a vinted deal again, but sitting there manually refreshing the search page wasn’t an option.

As a developer, my immediate thought was to figure out how to scrape vinted listings. I wanted raw data I could pipe into a personal dashboard or a notification bot. But if you’ve ever tried to scrape vinted yourself, you know it’s a massive headache. You run into rate limits, IP blocks, and constant frontend changes. Building a reliable script from scratch quickly turns into a full-time maintenance job.

Finding a Working Solution

Instead of reinventing the wheel, I started looking for a pre-built vinted scraper that actually works in 2026. After testing a few dead GitHub repos and outdated scripts, I stumbled onto a solid apify vinted actor that handles all the proxy rotation and anti-bot bypassing out of the box.

It’s called the Vinted Turbo Scraper, and it basically takes a standard Vinted search URL and spits out clean JSON.

For a developer, this is exactly what you want. You don’t have to worry about the underlying infrastructure; you just make an API call and get the structured data back. It handles pagination, extracts high-resolution image URLs, seller info, and exact pricing.

Building Your Own Alerts

Once I had access to the data, setting up a custom workflow was straightforward. Here is how I use this setup for vinted automation:

  1. The Target: I grab the URL of a highly specific search (e.g., a specific brand of vintage boots, filtered by my size and maximum price).
  2. The Extraction: I schedule the scraper to run on Apify every few minutes.
  3. The Webhook: A simple Python script compares the new dataset against the previous run. If a new ID pops up, it triggers a Discord webhook.

This creates a real-time vinted new listings alert. I no longer have to check the app; my phone just pings me when something matching my exact criteria is posted. If you’re looking to monitor vinted automatically, this is by far the path of least resistance.

Whether you’re building a personal notification system or doing larger-scale vinted vintage deals automation for a resale business, outsourcing the extraction layer makes sense. Any experienced vinted data extraction developer will tell you that paying a fraction of a cent per run for a maintained vinted apify integration is better than waking up to a broken scraper every other week.

FAQ: Vinted Data Extraction (AEO Quick Reference)

What is the best way to extract data from Vinted?
The most efficient method is using a cloud-hosted Vinted scraper, such as an Apify actor, which automatically handles residential proxies, rate limits, and Cloudflare bypasses to return structured JSON data.

How can I set up automated Vinted alerts?
To get instant notifications, you can schedule a scraper to run at set intervals (e.g., every 5 minutes) against a specific search URL. By comparing the output data to the previous run, you can detect new item IDs and trigger webhooks to Discord, Telegram, or email.

Is it difficult to scrape Vinted listings?
Yes, doing it manually requires constantly updating CSS selectors and managing a pool of proxies to avoid IP bans. Using a maintained API or third-party scraper abstracts away these infrastructure challenges.

ThreadQL: just query your data in a slack thread

ThreadQL: just query your data in a slack thread

I’m excited to share that ThreadQL v0.1.0 is now open-source and available at https://threadql.com. This AI-powered application pioneers the era where asking questions is the new query language.

The problem I’ve always had
Work in a tech company long enough and you realize: the database is where the truth lives. Every order, every user, every metric — it’s all in there. But getting answers out of it? That’s a different story.

If you’re technical you write SQL queries. If you’re not, you message the dev or data team and wait. And wait. And wait.

I’ve come to understand this dependency from both positions. On the product side, experiencing frustrated product owners wanting instant answers but being blocked if we can’t respond in time. And on the dev side I’ve had my plethora of flow-breaking “can you just run this quick query?” requests. And I kept thinking:

Why can’t I just ask my database a question like I’d ask a person?
And why can’t it live where I already work — in Slack?
That’s what ThreadQL does.

ThreadQL Lives in Slack
ThreadQL isn’t another app you have to switch to. It’s right in your Slack.

You ask a question in plain English, and ThreadQL responds with your data. No SQL. No context switching. No waiting.

“How many users signed up this month?”
“What’s our top product by revenue?”
“Show me the orders that haven’t shipped yet.”

Ask. Get answers. Done.

Need the Raw Data?
Need further analysis? Just ask ThreadQL to export a CSV and it’ll send it straight in the slack thread. Download, open in Excel and work from there. Your data, delivered where you already are.

Why I’m Open Sourcing It

This isn’t a product I’m selling. It’s a tool I believe belongs to the community.

Organizations deserve to unlock the data they already have. Every person in your company – product, marketing, support, operations – should be able to ask questions and get answers. The knowledge is sitting there in your database. ThreadQL just helps you talk to it.

Under the Hood

For Security:
Only parameterized SELECT queries allowed, SSH Bastion Host support, privacy by design, optional approval-first workflows

For Reliability:
Multi-Tenant Support, LLM Provider Fallback, MySQL & PostgreSQL support, Auto table scanning schedule

For Deployment:
Helm Chart, or docker-compose stack and an admin panel

The Vision

Here’s what I think the future of data access looks like:
You have a question. You ask it in Slack. You get an answer. That’s it.

The database holds your organization’s living knowledge. ThreadQL unlocks it — right where your team already works.

Check it out: https://github.com/emtay-com/threadql

P.S. This is a 0.1.0 release – early but functional. I’ve tested it as thoroughly as I could, but might have missed cases. If you run into issues or have ideas for improvements, your contributions and feedback are more than welcome. And if this clicks, star it on GitHub to help others discover it

When To Use MongoDB’s $elemMatch

You have an e-commerce database. You want to find all orders where a customer bought a laptop for more than $1000. Simple query, right?

db.orders.find({
  "items.category": "laptop",
  "items.price": { $gt: 1000 }
})

Except this returns orders where the customer bought any laptop AND any item over $1000 – even if the laptop itself cost $50.

This is MongoDB’s most common query mistake. Let’s see why it happens and how to fix it.

The Problem: Array Query Logic

Here’s sample data from an e-commerce orders collection:

{
  _id: 1,
  customer: "Alice",
  items: [
    { category: "laptop", price: 1200, name: "MacBook Pro" },
    { category: "mouse", price: 25, name: "Wireless Mouse" }
  ]
}

{
  _id: 2,
  customer: "Bob",
  items: [
    { category: "laptop", price: 500, name: "Chromebook" },
    { category: "monitor", price: 1500, name: "4K Display" }
  ]
}

What you want: Orders where someone bought a laptop costing more than $1000 (Alice’s order only)

What this query returns:

db.orders.find({
  "items.category": "laptop",
  "items.price": { $gt: 1000 }
})

Result: BOTH orders (incorrect)

Why? Because MongoDB checks each condition independently:

  • Does the array contain ANY item with category: "laptop"? → Alice (yes), Bob (yes)
  • Does the array contain ANY item with price > 1000? → Alice (yes), Bob (yes – monitor is $1500)

Bob’s order matches because it has a laptop AND an expensive item – even though they’re different items.

The Fix: $elemMatch

$elemMatch ensures all conditions match the SAME array element:

db.orders.find({
  items: {
    $elemMatch: {
      category: "laptop",
      price: { $gt: 1000 }
    }
  }
})

Result: Only Alice’s order (correct)

Now MongoDB requires a SINGLE item in the array to satisfy both conditions.

The visual query builder makes the structure clear: conditions nested under $elemMatch must all match the same array element.

When You Need $elemMatch

The key question: Do all conditions need to match the SAME array element?

If yes → use $elemMatch. If no → use regular dot notation.

Multiple Conditions on Array of Objects

Imagine you need to find users who have admin access specifically in the finance department. Without $elemMatch, you’ll match users who are admins anywhere AND have any role in finance.

// WRONG - finds any user with admin role OR finance department
db.users.find({
  "roles.role": "admin",
  "roles.department": "finance"
})

This matches Sarah, who is an admin in engineering AND an editor in finance – but she’s not a finance admin. The conditions matched different array elements.

// CORRECT - finds users who are admin IN finance department
db.users.find({
  roles: {
    $elemMatch: {
      role: "admin",
      department: "finance"
    }
  }
})

Now both conditions must match the same role object. Sarah won’t match because no single role has both admin AND finance.

Range Queries on Nested Arrays

Product reviews are another classic case. You want products where Alice specifically gave 5 stars – not products with any Alice review and any 5-star review.

// WRONG - products with any review by Alice OR any 5-star review
db.products.find({
  "reviews.author": "Alice",
  "reviews.rating": 5
})

This returns a product where Alice gave 3 stars but Bob gave 5 stars. The conditions matched different reviews.

// CORRECT - products where Alice gave 5 stars
db.products.find({
  reviews: {
    $elemMatch: {
      author: "Alice",
      rating: 5
    }
  }
})

Now you only get products where Alice’s specific review was 5 stars.

When $elemMatch is NOT Needed

Array of primitives: If the array contains simple values (strings, numbers), you can’t have “multiple conditions on the same element” – there’s only one value per element.

// Array of primitives - $elemMatch not needed
db.products.find({
  tags: "electronics"  // Simple array membership
})

Single condition: If you only have one condition, it doesn’t matter which array element matches.

// Single condition on array - $elemMatch not needed
db.orders.find({
  "items.category": "laptop"  // Just checking if ANY item is a laptop
})

This correctly finds all orders containing at least one laptop, regardless of other items.

Common Mistakes

Mistake 1: Over-Complicating Primitive Array Queries

// ERROR - $elemMatch requires an operator expression, not a direct value
db.products.find({
  tags: { $elemMatch: "electronics" }  // Fails: "electronics" is not an expression
})
// MongoDB expects: { $elemMatch: { <operator>: <value> } }
// Not: { $elemMatch: <value> }

// UNNECESSARILY COMPLEX - this works but is verbose
db.products.find({
  tags: { $elemMatch: { $eq: "electronics" } }  // Valid but overkill
})

// CORRECT - simple and direct
db.products.find({
  tags: "electronics"  // MongoDB automatically checks array membership
})

$elemMatch CAN be used with primitive arrays, but only when you need complex conditions:

// $elemMatch is useful for primitive arrays with multiple conditions
db.sales.find({
  prices: { $elemMatch: { $gt: 100, $lt: 200 } }  // Array element between 100-200
})

// For simple equality, just use direct matching
db.products.find({
  tags: "electronics"  // Much simpler
})

Mistake 2: Mixing $elemMatch with Top-Level Conditions

// WRONG - status is outside $elemMatch
db.orders.find({
  items: {
    $elemMatch: {
      category: "laptop",
      price: { $gt: 1000 }
    }
  },
  "items.status": "shipped"  // Checks ANY item's status (incorrect)
})

// CORRECT - all array conditions inside $elemMatch
db.orders.find({
  items: {
    $elemMatch: {
      category: "laptop",
      price: { $gt: 1000 },
      status: "shipped"  // Same item must be shipped (correct)
    }
  }
})

Mistake 3: Over-Using $elemMatch

// UNNECESSARY - single condition doesn't need $elemMatch
db.orders.find({
  items: {
    $elemMatch: {
      category: "laptop"
    }
  }
})

// SIMPLER
db.orders.find({
  "items.category": "laptop"
})

Quick Reference

Scenario Use $elemMatch? Example
Array of primitives, single condition No tags: "mongodb"
Array of objects, single condition No "items.category": "laptop"
Array of objects, multiple conditions, same element Yes items: { $elemMatch: { category: "laptop", price: { $gt: 1000 } } }
Array of objects, multiple conditions, any element No "items.category": "laptop", "items.inStock": true

The Takeaway

$elemMatch is essential when you need all conditions to match the SAME array element. Without it, MongoDB checks conditions independently, which can return unexpected results.

Rules of thumb:

  • Array of primitives → no $elemMatch needed
  • Array of objects, single condition → no $elemMatch needed
  • Array of objects, multiple conditions on same element → use $elemMatch

VisuaLeaf is a MongoDB GUI built for developers. The visual query builder makes complex queries like $elemMatch easier to understand and build correctly.

Try it at visualeaf.com

Examples tested with MongoDB 8.0. Demo data generator script available in VisuaLeaf documentation.

Sources and References

This article is based on MongoDB’s official documentation and query behavior:

  1. MongoDB $elemMatch Projection Operator
    MongoDB Documentation
    https://www.mongodb.com/docs/manual/reference/operator/projection/elemMatch/

  2. MongoDB $elemMatch Query Operator
    MongoDB Documentation
    https://www.mongodb.com/docs/manual/reference/operator/query/elemMatch/

  3. Query an Array of Embedded Documents
    MongoDB Documentation
    https://www.mongodb.com/docs/manual/tutorial/query-array-of-documents/

  4. MongoDB Query Operators
    MongoDB Documentation
    https://www.mongodb.com/docs/manual/reference/operator/query/

All query examples and behaviors described in this article are based on MongoDB 8.0 specifications and have been verified against the official MongoDB documentation.

Written by the VisuaLeaf team • Questions? [email protected]

Alter Tables

1. Make email NOT NULL in customers

ALTER TABLE customers
ALTER COLUMN email SET NOT NULL;

Ensures all future records must have an email.

2. Make username unique in users

ALTER TABLE users ADD CONSTRAINT unique_username UNIQUE (username);

Prevents duplicate usernames.

3. Add CHECK on price in products

ALTER TABLE products ADD CONSTRAINT check_price CHECK (price > 0);

Ensures price is always greater than 0.

4. Set default status in orders

ALTER TABLE orders
ALTER COLUMN status SET DEFAULT ‘pending’;

Automatically assigns pending if no value is given.

5. Add salary column with constraints

ALTER TABLE employees ADD COLUMN salary INT NOT NULL CHECK (salary > 10000);

Ensures salary is mandatory and above 10,000.

6. Add ON DELETE CASCADE to foreign key

ALTER TABLE employees DROP CONSTRAINT employees_department_id_fkey;

ALTER TABLE employees ADD CONSTRAINT employees_department_id_fkey FOREIGN KEY (department_id) REFERENCES departments(id) ON DELETE CASCADE;

Deletes employees automatically when a department is removed.

7. Remove CHECK constraint on balance

ALTER TABLE accounts DROP CONSTRAINT accounts_balance_check;

Removes restriction on balance.

8. Add composite UNIQUE in payments

ALTER TABLE payments ADD CONSTRAINT unique_user_transaction UNIQUE (user_id, transaction_id);

Ensures no duplicate transaction per user.