<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Code Like a Woman]]></title><description><![CDATA[Making modern software engineering concepts feel approachable through simple mental models and practical lessons learned.]]></description><link>https://codelikeawoman.com</link><generator>RSS for Node</generator><lastBuildDate>Mon, 07 Sep 2026 23:47:46 GMT</lastBuildDate><atom:link href="https://codelikeawoman.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Three Cloud Access-Control Limits That Might Surprise You When Migrating Services at Scale]]></title><description><![CDATA[The principle of least privilege is an access-control approach in which a user or service receives only the permissions needed to perform its job.
For example, a service that reads files from a specif]]></description><link>https://codelikeawoman.com/three-cloud-access-control-limits-that-might-surprise-you-when-migrating-services-at-scale</link><guid isPermaLink="true">https://codelikeawoman.com/three-cloud-access-control-limits-that-might-surprise-you-when-migrating-services-at-scale</guid><category><![CDATA[Cloud Computing]]></category><category><![CDATA[cloud security]]></category><category><![CDATA[Cloud Migration]]></category><category><![CDATA[identity and access management ]]></category><category><![CDATA[System Design]]></category><dc:creator><![CDATA[Mirna De Jesus Cambero]]></dc:creator><pubDate>Tue, 01 Sep 2026 15:56:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67c0bd8498b78e6abd0ca1ae/9ed5ca31-a4eb-4af4-badb-c026c1c321d2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The principle of least privilege is an access-control approach in which a user or service receives only the permissions needed to perform its job.</p>
<p>For example, a service that reads files from a specific group of storage buckets can be given access to only those buckets instead of the entire project. If its credentials are exposed or the service is compromised, the potential impact is limited to a smaller set of resources.</p>
<p>This makes least privilege useful for reducing the scope of security incidents and preventing accidental access or changes to unrelated resources.</p>
<p>In practice, implementing it often involves creating separate identities and narrowly scoped permissions. A service account might access only buckets with a specific prefix. A database user might access only one schema or a subset of tables. A Kafka producer might write only to the topics it uses.</p>
<p>These are just a few examples of least privilege in practice.</p>
<p>During a large cloud migration, these identities and permissions may need to be created for dozens or hundreds of services. That is when platform limits that rarely matter at a smaller scale can become a problem.</p>
<p>Some limits can be increased. Others depend on the pricing tier. And some require a different architecture.</p>
<p>Here are three examples of access-control limits you might encounter during a large-scale migration—and some options for working around them.</p>
<h2>1. GCP’s 20 Conditional IAM Bindings Limit</h2>
<p>Google Cloud IAM Conditions let you restrict a principal’s access based on attributes such as a resource’s name or type.</p>
<p>Imagine a migration service account that needs to read only from Cloud Storage buckets with a particular prefix. A conditional binding can grant it the Storage Object Viewer role only when the bucket name matches that prefix.</p>
<p>This is a conditional binding in action: the principal and role remain the same, but a condition limits where the access applies.</p>
<p>However, GCP allows only 20 bindings in the same allow policy when they have:</p>
<ul>
<li><p>The same principal</p>
</li>
<li><p>The same role</p>
</li>
<li><p>Different conditions</p>
</li>
</ul>
<p>This is a fixed limit. It cannot be increased.</p>
<p>If the same service account and role require separate conditions for different groups of resources, the first 20 bindings may work, but adding the 21st can fail.</p>
<p>Once that happens, the permission model must change. One option is to grant the role directly at the resource level instead of continuing to add conditional bindings to the project-level policy.</p>
<p>A permission model that works well for a few groups of resources may not work at a larger scale.</p>
<p><a href="https://docs.cloud.google.com/iam/quotas">Google Cloud: IAM quotas and limits</a></p>
<h2>2. MongoDB Atlas’s 100 Database Users Limit</h2>
<p>Creating a separate MongoDB Atlas user for each service gives every service its own credentials and permissions.</p>
<p>For example, an orders service may have read and write access to order-related collections, while a reporting service receives read-only access to a smaller subset of data. Their credentials can also be rotated or revoked independently.</p>
<p>MongoDB Atlas allows up to 100 database users per project.</p>
<p>Imagine migrating 80 services and creating one database user for each. That already uses 80 of the 100 available users.</p>
<p>Now add developer accounts, CI/CD identities, operational tools, legacy integrations, and temporary migration jobs. The remaining 20 users can disappear quickly.</p>
<p>MongoDB recommends contacting Atlas Support when more than 100 users are needed. However, its public documentation does not guarantee an increase or specify a higher maximum.</p>
<p>Possible first steps include removing unused users and reviewing whether any identities can be consolidated without weakening the intended access controls. If that is not enough, contact Atlas Support early to understand what options are available beyond 100 users.</p>
<p><a href="https://www.mongodb.com/docs/atlas/security-add-mongodb-users/">MongoDB Atlas: Configure Database Users</a></p>
<h2>3. Confluent Cloud’s Kafka RBAC Role Binding Quotas</h2>
<p>Confluent Cloud uses RBAC role bindings to control which Kafka resources a principal can access.</p>
<p>Imagine an order-processing service that:</p>
<ul>
<li><p>Consumes from the <code>orders-created</code> topic</p>
</li>
<li><p>Uses the <code>order-processing</code> consumer group</p>
</li>
<li><p>Publishes to the <code>orders-processed</code> topic</p>
</li>
</ul>
<p>Its service account may need separate role bindings for each type of access. This allows the service to consume and publish the messages it needs without granting access to unrelated topics or consumer groups.</p>
<p>The number of cluster-level RBAC role bindings with Kafka permissions depends on the cluster type:</p>
<ul>
<li><p>Basic, Standard, Enterprise, and Freight: 500 per cluster</p>
</li>
<li><p>Dedicated: 25,000 per cluster</p>
</li>
</ul>
<p>If a service needs an average of three bindings, 170 services in the same Kafka cluster could require around 510—more than the 500-binding limit available on several cluster types.</p>
<p>The exact number will vary, but the multiplication is easy to miss when permissions are created one service at a time.</p>
<p>Possible options include consolidating bindings through shared resource prefixes where appropriate, requesting a quota increase if one is available, or moving to a Dedicated cluster. The last option can significantly affect cost.</p>
<p>Access-control requirements should therefore be considered when choosing a cluster, not only throughput and storage.</p>
<p><a href="https://docs.confluent.io/cloud/current/quotas/service-quotas.html">Confluent Cloud: Service quotas</a></p>
<h2>Final Thoughts</h2>
<p>Least privilege is worth aiming for. By limiting what each identity can access, it can improve the security of your services and production environments. However, the identities and permissions used to implement this it are not unlimited.</p>
<p>Before migrating services at scale, count the services and resources that will need access. Estimate how many users, identities, and role bindings that access model will require.</p>
<p>Then design a model that remains granular without exceeding the platform’s limits. If the numbers do not fit, determine early whether the limit can be increased, requires a higher tier, or calls for a different design.</p>
<p>Finding a limit early gives you options. Finding it mid-migration can mean delays, redesigns, or unexpected costs.</p>
]]></content:encoded></item><item><title><![CDATA[Everything “as a Service”: Making Sense of the Cloud]]></title><description><![CDATA[If you've spent any time around cloud computing, you've probably noticed how often the words “as a Service” come up.
Something as a Service. Something else as a Service. And usually, there's an acrony]]></description><link>https://codelikeawoman.com/everything-as-a-service-making-sense-of-the-cloud</link><guid isPermaLink="true">https://codelikeawoman.com/everything-as-a-service-making-sense-of-the-cloud</guid><category><![CDATA[Cloud Computing]]></category><category><![CDATA[AWS]]></category><category><![CDATA[gcloud]]></category><category><![CDATA[FaaS]]></category><category><![CDATA[IaaS]]></category><category><![CDATA[PaaS]]></category><category><![CDATA[SaaS]]></category><category><![CDATA[serverless]]></category><dc:creator><![CDATA[Mirna De Jesus Cambero]]></dc:creator><pubDate>Fri, 28 Aug 2026 18:27:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67c0bd8498b78e6abd0ca1ae/edcea06b-e07f-4097-9697-97532d5422aa.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you've spent any time around cloud computing, you've probably noticed how often the words <strong>“as a Service”</strong> come up.</p>
<p>Something as a Service. Something else as a Service. And usually, there's an acronym to go with it.</p>
<p>So what do they actually mean?</p>
<p>Let's look at the problem each one solves, what you manage, and what the provider manages for you.</p>
<h2>First, what does “as a Service” mean?</h2>
<p>At its core, “as a Service” means that instead of owning, building, or managing something yourself, <strong>a provider offers it to you as something you can use.</strong></p>
<p>Think about email.</p>
<p>A company could build its own email application, run the servers behind it, store all the messages, maintain the software, handle security updates, and keep everything available. Or it could simply use Gmail, while Google takes care of most of what is required to make the email service work.</p>
<p>Cloud service models apply this same idea at different levels.</p>
<h2>IaaS — Infrastructure as a Service</h2>
<p>Your application needs a computer somewhere to run on.</p>
<p>Traditionally, that could mean buying a physical server, finding somewhere to keep it, connecting it to the network, maintaining it, replacing hardware when it fails, and buying more servers when you need more capacity.</p>
<p>But what if you could simply rent the computing infrastructure you need?</p>
<p>That's essentially <strong>Infrastructure as a Service (IaaS).</strong> Instead of owning the physical machines, you get computing resources from a cloud provider.</p>
<p>With Infrastructure as a Service, your cloud provider allows you to create virtual machines to run your applications. The provider takes care of the physical hardware, while you still manage the operating system, installed software, and your application.</p>
<p>In plain English: <strong>give me a computer to run my stuff on without making me own the physical computer.</strong></p>
<p>On AWS, <strong>Amazon EC2</strong> is an IaaS offering. On Google Cloud, <strong>Compute Engine</strong> fills a similar role.</p>
<h2>PaaS — Platform as a Service</h2>
<p>IaaS saves you from owning the physical infrastructure. But what if you don't want to manage the virtual machine either?</p>
<p>Maybe what you really want to say is: <strong>Here's my application. Please run it.</strong></p>
<p>That's the basic idea behind <strong>Platform as a Service (PaaS).</strong></p>
<p>With PaaS, you deploy your application while the platform takes care of much of what is required to run it. You don't need to start by creating a server, installing an operating system, configuring a web server, and setting up all the infrastructure yourself.</p>
<p>A classic example is <strong>Heroku</strong>: you deploy your application, and Heroku takes care of much of what is needed to run it.</p>
<p>The same idea exists within the major cloud providers. <strong>Elastic Beanstalk</strong> is an AWS PaaS offering, while <strong>App Engine</strong> provides a PaaS option on Google Cloud.</p>
<p>In plain English: <strong>here's my application—run it for me.</strong></p>
<h2>FaaS — Function as a Service</h2>
<p>Sometimes you don't need an entire application running all the time. You have a piece of code that only needs to run when something happens.</p>
<p>For example:</p>
<ul>
<li><p>An email is received → process it.</p>
</li>
<li><p>A photo is uploaded → resize it.</p>
</li>
<li><p>A user signs up → send a welcome email.</p>
</li>
<li><p>A payment is completed → generate a receipt.</p>
</li>
</ul>
<p>In other words, you want the cloud to <strong>perform an action on your behalf when something happens</strong>.</p>
<p>You could keep a server running all day waiting for one of these events. Or you could simply say: <strong>when this happens, run this function.</strong></p>
<p>That's the basic idea behind <strong>Function as a Service (FaaS).</strong></p>
<p>With FaaS, you provide the function and the cloud provider takes care of the infrastructure needed to run it. You don't choose a server or keep one running yourself. You focus primarily on the code you want to execute.</p>
<p>This approach is commonly known as <strong>serverless computing</strong>. Serverless doesn't mean there are no servers. There are—you're simply not responsible for managing them.</p>
<blockquote>
<p>💡 <strong>Tip:</strong> FaaS is one form of serverless computing, but <strong>serverless is a broader term</strong> that includes other types of cloud services as well. We'll explore that distinction in a future article.</p>
</blockquote>
<p>On AWS, <strong>Lambda</strong> is a FaaS offering. On Google Cloud, <strong>Cloud Run functions</strong> provides this capability.</p>
<p>In plain English: <strong>when this happens, run this code for me.</strong></p>
<h2>SaaS — Software as a Service</h2>
<p>What if you don't want to manage the infrastructure, deploy an application, or write the code? You just want to use the software.</p>
<p>That's <strong>Software as a Service (SaaS).</strong></p>
<p>With SaaS, someone else builds, hosts, and operates the software. You simply access and use it.</p>
<p>Gmail is a good example. You don't need to build an email application, maintain an email server, or worry about the infrastructure behind it. You simply use Gmail while Google takes care of running and maintaining it.</p>
<p>The same is true of products like Slack, Dropbox, Salesforce, and many of the software tools we use every day.</p>
<p>SaaS is commonly subscription-based. You might pay for the subscription yourself, or your company might pay for it and give you access.</p>
<blockquote>
<p>💡 <strong>Tip:</strong> Subscription is common, but it's not what makes software SaaS. SaaS can also be free.</p>
</blockquote>
<blockquote>
<p>💡 <strong>Tip:</strong> Installing software on your device doesn't mean it isn't SaaS. Slack, for example, is still SaaS when you use its desktop or mobile app. The installed app is simply one way to access the software that Slack hosts and operates.</p>
</blockquote>
<p>In plain English: <strong>I just want to use the software. Someone else takes care of running it.</strong></p>
<h2>Cloud “as a Service” at a Glance</h2>
<p>If you want a quick way to remember how these models differ, think about <strong>how much you're responsible for managing versus how much the provider manages for you.</strong></p>
<img src="https://cdn.hashnode.com/uploads/covers/67c0bd8498b78e6abd0ca1ae/5d6dc44f-53ae-4b0a-b81e-76bfe125c797.png" alt="" style="display:block;margin:0 auto" />

<h2>Final Thoughts</h2>
<p>The next time you encounter another “as a Service,” try asking:</p>
<p><strong>What problem does it solve? What is being provided? And what will I be responsible for managing?</strong></p>
<p>Answering those questions can make the acronym much easier to understand—and remember.</p>
]]></content:encoded></item><item><title><![CDATA[AI Made Software Development Easier. Here’s What Got Harder.]]></title><description><![CDATA[AI-assisted development has changed how we build software. We can explore unfamiliar codebases, work across languages and frameworks, debug problems, write tests, understand existing implementations, ]]></description><link>https://codelikeawoman.com/ai-made-software-development-easier-here-s-what-got-harder</link><guid isPermaLink="true">https://codelikeawoman.com/ai-made-software-development-easier-here-s-what-got-harder</guid><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[AI]]></category><category><![CDATA[AI-assisted development]]></category><category><![CDATA[developer productivity]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[software development]]></category><dc:creator><![CDATA[Mirna De Jesus Cambero]]></dc:creator><pubDate>Sun, 23 Aug 2026 08:02:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67c0bd8498b78e6abd0ca1ae/29f94b85-773a-4796-9682-aa368d512eb4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>AI-assisted development has changed how we build software. We can explore unfamiliar codebases, work across languages and frameworks, debug problems, write tests, understand existing implementations, and build features significantly faster than before.</p>
<p>Software development <strong>is easier because of AI</strong>. But making something easier doesn't mean removing all the challenges. Sometimes it means replacing old ones with completely different ones.</p>
<p>After working this way for a while, I've realized there are quite a few new things we're learning to deal with.</p>
<h2>The productivity bar keeps getting higher</h2>
<p>AI makes developers faster, but as AI increases how quickly developers can deliver, expectations around productivity increase with it. If something used to take a week and can now be done in three days, eventually three days becomes the expectation.</p>
<p>The time AI saves doesn't necessarily become extra time for understanding an implementation, improving tests, reviewing architecture, or learning something new. Sometimes it just creates room for <strong>more work</strong>.</p>
<p>When the expectation is to continuously produce more, deeply understanding everything you're shipping can start feeling optional. The feature works. The tests pass. AI can explain the implementation whenever you need it. Do you really need to understand every piece of it?</p>
<p>Probably not. But understanding almost none of it isn't a great alternative either. <strong>The challenge is finding the right balance between understanding every line of code AI produces and having only a high-level understanding of what you're shipping.</strong> Where that balance sits will depend on the change, its complexity, and the risk involved.</p>
<h2>We can produce code faster than humans can review it</h2>
<p>AI can generate a substantial amount of code incredibly quickly. But when a human reviews that code, they still need to understand what changed and whether it makes sense.</p>
<p>That can mean more PRs, larger PRs, and more code for reviewers to process. <strong>The amount of code we can produce has increased much faster than our capacity to deeply review it.</strong></p>
<h2>AI reviewers can make "done" harder to reach</h2>
<p>AI code reviewers can help us keep up with the increasing amount of code being produced by identifying vulnerabilities, missing validations, defects, unhandled failures, and edge cases we might otherwise miss. <strong>They don't replace human reviews, but they add another valuable layer of review—and another layer of feedback to get through.</strong></p>
<p>Fix comment. Push. New review. Fix another comment. Push. New review. Now there are three more things to consider.</p>
<p>Eventually you're wondering: <strong>Are we meaningfully improving this feature, or are we just trying to make the bot happy?</strong></p>
<p>An implementation that handles the most common cases and provides value can be exactly the right engineering decision. We can ship, learn from how the feature is actually used, and address additional edge cases as we iterate instead of trying to solve everything upfront.</p>
<h2>We can build things we don't fully understand</h2>
<p>Historically, implementing something generally required you to understand it well enough to write it. Those two things are no longer tightly coupled.</p>
<p>Today you can work in a language you barely know, use an unfamiliar framework, or implement a pattern you only partially understand. AI can explore the codebase, propose a solution, write the implementation, generate the tests, and fix the errors.</p>
<p>That's incredibly powerful, but it creates a strange situation: <strong>our ability to produce sophisticated solutions can exceed our ability to independently evaluate them.</strong></p>
<h2>Being polyglot is easier. Being an expert isn't.</h2>
<p>AI has dramatically lowered the barrier to working across technologies. Need some Python? Terraform? A Kubernetes configuration you've never written before? AI can get you surprisingly far.</p>
<p>But there's an important difference between <strong>being able to build something with a technology</strong> and <strong>understanding that technology well enough to recognize when something is subtly wrong</strong>.</p>
<p>The further we move outside our areas of expertise, the more we're relying on AI not only to produce the implementation but sometimes to tell us whether its own implementation is good.</p>
<h2>Giving AI the right context is now part of the job</h2>
<p>The solution AI proposes can change dramatically depending on the context available to it. Give it access to one repository and it may propose a perfectly reasonable solution. Add another repository involved in the same flow, and the right solution might look completely different.</p>
<p><strong>AI can reason about the context it has, but it doesn't necessarily know what important context it's missing.</strong></p>
<p>Even when you start with the right context, keeping it throughout a complex implementation can be challenging. Conversations get long, context gets compacted, and important requirements, architectural decisions, or conventions can get lost.</p>
<p>Part of AI-assisted development is now <strong>context management</strong>: making sure AI has the right information to begin with and that important context isn't lost along the way.</p>
<h2>Sometimes you have to look beyond AI</h2>
<p>AI can be incredibly good at debugging, but I've run into difficult issues where it keeps pointing me toward the same set of possible causes and solutions. You try them, provide more information, and somehow end up back in the same place.</p>
<p>At some point, you have to break that cycle. I've had to step away from the AI conversation, search the web, dig through Stack Overflow, or read documentation to find information that leads to a different hypothesis or potential root cause. <strong>From there, you can bring AI along again, this time in a new direction.</strong></p>
<h2>AI can be confidently wrong</h2>
<p>Sometimes AI confidently tells you that a library behaves a certain way when it doesn't. Sometimes it describes functionality that doesn't exist. Sometimes the answer is mostly correct but misses the piece of nuance that actually matters for your problem.</p>
<p>The more frequently AI gives us correct answers, the easier it becomes to trust the incorrect ones too. Knowing when <strong>not</strong> to trust the answer is becoming an increasingly important engineering skill.</p>
<h2>We're becoming less familiar with our own codebases</h2>
<p>When I manually explored a codebase and implemented something, I naturally built a mental map along the way. I remembered where things lived, which service called what, and that weird piece of logic because I had spent two hours debugging it.</p>
<p>An agent can now search the repository, identify the relevant files, explain them, modify several of them, run the tests, and get the feature working. I understood what happened at the time, but ask me two weeks later where that logic lives? I might have to ask AI again.</p>
<p>We can interact with much larger portions of a codebase without necessarily developing the same familiarity with them. We can ship more changes while accumulating <strong>less knowledge per change</strong>.</p>
<h2>Understanding something doesn't mean we'll remember it</h2>
<p>AI explains something. I read it. Everything clicks. <em>Ahhh. That makes perfect sense.</em> At that moment, I genuinely understand it. A few weeks later? I barely remember it.</p>
<p>Some of the friction AI removes—searching through documentation, experimenting, getting something wrong, debugging it, trying again—is also the friction that used to make concepts stick.</p>
<p>Removing that friction makes us faster, but there's a tradeoff there that I don't think we fully understand yet.</p>
<h2>Our debugging skills are getting less practice</h2>
<p>When something breaks now, one of my first instincts is to ask AI. And why wouldn't it be? AI can inspect logs, trace execution paths, search the codebase, generate hypotheses, and frequently find the problem faster than I could manually.</p>
<p>But debugging, reading unfamiliar code, forming hypotheses, and knowing where to look when the obvious solution isn't working are skills developed through repetition. <strong>If AI performs more and more of those repetitions for us, what happens when we encounter the problem it can't solve?</strong></p>
<h2>Technical debt can move faster too</h2>
<p>Outdated documentation, poor abstractions, inconsistent conventions, and technical debt all existed long before AI-assisted development. <strong>What AI can change is the speed at which they accumulate.</strong></p>
<p>When a team can generate and ship significantly more code, bad patterns can spread faster too. Documentation can become outdated faster, architectural inconsistencies can multiply faster, and developers working across technologies they don't deeply understand can unintentionally introduce patterns that don't quite belong.</p>
<p>AI didn't invent these problems but It can accelerate them.</p>
<h2>Final Thoughts</h2>
<p>AI-assisted development has made our jobs easier, but it has also introduced challenges we didn't have before.</p>
<p>The interesting question now is <strong>how we navigate those challenges without losing the engineering judgment and skills we still need.</strong></p>
<p>That's something I'm still figuring out—and perhaps something we need to talk more about.</p>
<p><strong>What about you? What challenges have you run into since you started using AI for development? Do any of these resonate? I'd love to know.</strong></p>
]]></content:encoded></item><item><title><![CDATA[Web Application Security: 10 Fundamentals and Why They Matter]]></title><description><![CDATA[Web application security can sound like a highly specialized topic. And it can be but underneath more advanced security practices are a number of fundamentals that show up in almost every web applicat]]></description><link>https://codelikeawoman.com/web-application-security-10-fundamentals-and-why-they-matter</link><guid isPermaLink="true">https://codelikeawoman.com/web-application-security-10-fundamentals-and-why-they-matter</guid><category><![CDATA[Web Security]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[cybersecurity]]></category><category><![CDATA[Application Security]]></category><category><![CDATA[Software Engineering]]></category><dc:creator><![CDATA[Mirna De Jesus Cambero]]></dc:creator><pubDate>Sat, 15 Aug 2026 23:09:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67c0bd8498b78e6abd0ca1ae/26028355-0cb7-4dfa-805f-af60f7b1d6cd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Web application security can sound like a highly specialized topic. And it can be but underneath more advanced security practices are a number of fundamentals that show up in almost every web application: how communication is protected, how access to data is controlled, how external input is handled, how credentials are stored, and how other systems are trusted.</p>
<p>Understanding these concepts provides a useful foundation for thinking about application security.</p>
<p>Here are 10 of them and the problems they're designed to solve.</p>
<h2>1. HTTPS and encryption in transit</h2>
<p>When a browser communicates with a web application, information travels across a network.</p>
<p>That information might include passwords, authentication cookies, personal information, or application data.</p>
<p><strong>HTTPS encrypts that communication while it travels between the browser and the server.</strong></p>
<p>Without HTTPS, someone capable of intercepting the traffic may be able to read or manipulate it.</p>
<p>Today, HTTPS is a standard part of deploying web applications. Modern hosting platforms and certificate authorities have also made obtaining and renewing TLS certificates considerably easier than it once was.</p>
<blockquote>
<p>💡 <strong>What is a TLS certificate?</strong></p>
<p><strong>TLS stands for Transport Layer Security.</strong> A TLS certificate is a digital certificate that identifies the server behind a website. It allows the browser to verify that it is communicating with the expected server and helps establish an encrypted connection between them.</p>
<p>Once that secure connection is established, the information exchanged between the browser and the server is encrypted, preventing someone intercepting the traffic from simply reading it.</p>
</blockquote>
<p>The important idea is that sensitive information needs protection not only when it's stored, but also while it's moving between systems.</p>
<h2>2. Authentication and authorization</h2>
<p>Once a request reaches an application, two important questions are: <strong>who is making the request, and what are they allowed to do?</strong></p>
<p>Authentication and authorization solve these two related but different problems.</p>
<p><strong>Authentication</strong> answers:</p>
<blockquote>
<p>Who are you?</p>
</blockquote>
<p><strong>Authorization</strong> answers:</p>
<blockquote>
<p>What are you allowed to access?</p>
</blockquote>
<p>Imagine an online store where customers can log in and view their orders.</p>
<p>Sarah logs in successfully. The application verifies her credentials and now knows that the person making requests is Sarah.</p>
<p>That's <strong>authentication</strong>.</p>
<p>Sarah opens one of her orders, which is available through an endpoint like:</p>
<p><code>/orders/123</code></p>
<p>Order <code>123</code> belongs to Sarah, so she should be able to access it.</p>
<p>Now imagine order <code>124</code> belongs to another customer.</p>
<p>If Sarah changes the URL to:</p>
<p><code>/orders/124</code></p>
<p>and the application returns that order simply because she is logged in, there's a problem.</p>
<p>The application correctly authenticated Sarah—it knows exactly who she is—but it failed to check whether Sarah is <strong>authorized</strong> to access order <code>124</code>.</p>
<p>That's an authorization vulnerability.</p>
<p>This type of vulnerability is commonly associated with <strong>IDOR (Insecure Direct Object Reference)</strong>: an application exposes a reference to a resource, such as an order ID, but doesn't properly verify that the user making the request has permission to access that resource.</p>
<p>And this doesn't apply only to URLs or orders. The same principle applies whenever an application accesses a protected resource—documents, invoices, messages, projects, files, API resources, or anything else that one user shouldn't automatically be able to access just because they're authenticated.</p>
<p>Authentication establishes <strong>who is making the request</strong>.</p>
<p>Authorization determines <strong>whether that person should be allowed to make it</strong>.</p>
<h2>3. Input validation</h2>
<p>Knowing who sent a request doesn't mean the application can automatically trust what that request contains.</p>
<p>Applications constantly receive information from the outside world.</p>
<p>Form submissions, API requests, URL parameters, uploaded files, headers, and query parameters are all forms of external input.</p>
<p><strong>Input validation</strong> establishes what the application considers acceptable before that information is processed.</p>
<p>An email field, for example, can be checked for an expected format. A numeric value can be checked against an acceptable range.</p>
<p>But input validation isn't only about catching accidental mistakes.</p>
<p>Imagine an application expects a numeric product ID:</p>
<p><code>GET /products?id=123</code></p>
<p>An attacker could instead send unexpected input containing SQL code. If that input isn't handled safely, it could alter the database query the application executes. <strong>This type of attack is known as SQL injection.</strong></p>
<blockquote>
<p>💡 <strong>What is SQL injection?</strong></p>
<p>SQL injection is an attack where malicious input is used to alter a database query. This can allow an attacker to access, modify, or delete data they shouldn't have access to.</p>
</blockquote>
<p>Validating that the product ID is actually a number helps reject unexpected input. At the database layer, <strong>parameterized queries</strong> provide the primary protection against SQL injection by keeping user input separate from the SQL command itself.</p>
<p>The broader principle is simple: <strong>input coming from outside the application shouldn't automatically be trusted.</strong></p>
<h2>4. Secrets management</h2>
<p>Applications also have sensitive information of their own.</p>
<p>They often need credentials to communicate with databases, APIs, and other services.</p>
<p>These can include:</p>
<ul>
<li><p>API keys</p>
</li>
<li><p>Database credentials</p>
</li>
<li><p>Access tokens</p>
</li>
<li><p>Signing keys</p>
</li>
<li><p>Third-party service credentials</p>
</li>
</ul>
<p>These values are commonly referred to as <strong>secrets</strong>.</p>
<p>Secrets are generally kept separate from application code through environment variables or dedicated secrets-management systems.</p>
<p>Hardcoding them into source code creates a different problem: source code tends to be copied, shared, deployed, and stored in version-control systems.</p>
<p>And Git has history.</p>
<p>Removing a secret from the latest version of a file doesn't necessarily remove it from previous commits. This is why a credential accidentally committed to a repository is generally treated as compromised and rotated.</p>
<p>Secrets management is ultimately about controlling where sensitive credentials live and who—or what—can access them.</p>
<h2>5. Rate limiting</h2>
<p>Even a perfectly valid request can become a problem when it's repeated thousands of times.</p>
<p>That's where <strong>rate limiting</strong> comes in.</p>
<p>Rate limiting controls how frequently a user, client, or source can perform a particular action within a period of time.</p>
<p>Consider a login endpoint.</p>
<p>A normal user might enter the wrong password a few times.</p>
<p>An automated attacker could potentially try thousands of passwords.</p>
<p>Rate limiting can restrict how many attempts are allowed within a certain period, making this kind of automated abuse more difficult.</p>
<p>The same concept can apply to password resets, verification codes, account creation, API requests, and other operations that could be abused at scale.</p>
<p>Input validation asks:</p>
<blockquote>
<p>Is this request acceptable?</p>
</blockquote>
<p>Rate limiting asks:</p>
<blockquote>
<p>How often should this request be allowed?</p>
</blockquote>
<p>They protect against different problems, even though both deal with requests entering an application.</p>
<h2>6. Password hashing</h2>
<p>Authentication often relies on one particularly sensitive piece of information: a user's password.</p>
<p>Web applications typically don't store users' actual passwords.</p>
<p>Instead, passwords are stored as <strong>hashes</strong> generated using password-hashing algorithms such as bcrypt or Argon2.</p>
<blockquote>
<p>💡 <strong>What is hashing?</strong></p>
<p>Hashing is a one-way transformation that takes an input—like a password—and produces a fixed representation called a <strong>hash</strong>. The same input can be verified against the stored hash later, but the original password isn't meant to be recovered from it.</p>
<p>This is different from <strong>encryption</strong>, which is designed to be reversible with the right key.</p>
</blockquote>
<p>When a user logs in, the password they provide is passed through the same hashing process and checked against the stored password hash. If they match, the password is valid. At no point does the application need to store or recover the original password.</p>
<p>This becomes particularly important if a database is compromised.</p>
<p>If passwords were stored as plain text, anyone with access to the database could immediately read them. With properly hashed passwords, what gets exposed instead are the hashes, making recovering the original passwords significantly more difficult.</p>
<h2>7. Dependency vulnerabilities</h2>
<p>An application's security doesn't depend only on the code its developers write.</p>
<p>Modern applications rely heavily on third-party code.</p>
<p>Frameworks, database clients, authentication libraries, HTTP clients, and hundreds of smaller packages can become part of an application's dependency tree.</p>
<p>Those dependencies can contain security vulnerabilities too.</p>
<p>When vulnerabilities in third-party packages become publicly known, dependency-scanning tools can help identify whether an application is using an affected version.</p>
<blockquote>
<p>💡 <strong>What is a CVE?</strong></p>
<p><strong>CVE stands for Common Vulnerabilities and Exposures.</strong> A CVE is a standardized identifier assigned to a publicly known security vulnerability, giving everyone a common way to refer to the same vulnerability.</p>
<p>For example, a vulnerability might be identified as <code>CVE-2026-12345</code>.</p>
</blockquote>
<p>Dependency-scanning tools can compare the packages and versions used by an application against databases of known vulnerabilities. This makes it possible to identify cases where an application depends on a library version with a documented security problem.</p>
<p>Application security, therefore, isn't limited to the code an engineering team writes. It also includes the code the application depends on.</p>
<h2>8. Webhook and callback verification</h2>
<p>Applications also communicate with systems outside their own boundaries.</p>
<p>Webhooks are a common example.</p>
<p>Webhooks allow one system to notify another when something happens.</p>
<p>A payment provider might send:</p>
<p><code>payment.completed</code></p>
<p>An authentication provider might notify an application about a user event.</p>
<p>A third-party integration might send an update when external data changes.</p>
<p>But a webhook is ultimately an HTTP request.</p>
<p>Simply receiving a request at a particular endpoint doesn't prove that it came from the expected provider.</p>
<p>This is why webhook systems commonly include a mechanism for <strong>verifying authenticity</strong>, often through cryptographic signatures or shared secrets.</p>
<p>The receiving application can validate that information before trusting the payload.</p>
<p>Without verification, someone who discovers the webhook endpoint could potentially imitate the external service and send fabricated events.</p>
<p>Webhook verification establishes an important boundary of trust: not just <em>what does this request say?</em>, but <em>can the application trust who sent it?</em></p>
<h2>9. Automated security scanning</h2>
<p>Many of the problems we've discussed can leave traces that tools are capable of detecting automatically.</p>
<p>Static-analysis tools can inspect source code for potentially dangerous patterns.</p>
<p>Secret scanners can detect credentials that may have accidentally been committed.</p>
<p>Dependency scanners can identify packages associated with known vulnerabilities.</p>
<p>These checks can also be integrated into CI/CD pipelines so they run whenever code changes.</p>
<p>Automation doesn't guarantee that an application is secure, and automated tools can't understand every security implication of an application's design.</p>
<p>What they provide is another layer of detection.</p>
<p>Instead of relying entirely on someone remembering to check for every potential problem, repeatable checks can happen continuously as the software evolves.</p>
<h2>10. Backups, logging, and monitoring</h2>
<p>Not every security measure is about preventing something from happening.</p>
<p>Some are about understanding what happened and recovering when something goes wrong.</p>
<p><strong>Backups</strong> provide a way to recover data after accidental deletion, corruption, infrastructure failure, or certain types of security incidents.</p>
<p>But the existence of a backup doesn't necessarily mean the data is recoverable. Backup strategies also involve verifying that the stored data can actually be restored.</p>
<p><strong>Logging and monitoring</strong> address another part of the problem.</p>
<p>Logs can provide information about authentication attempts, application errors, access patterns, and important system events. Monitoring can then help surface unusual behavior or failures that require attention.</p>
<p>At the same time, logs themselves need thoughtful handling. Sensitive information such as passwords, authentication tokens, or secrets generally shouldn't end up there.</p>
<p>The goal isn't simply to prevent every possible incident. It's also to have enough visibility and recoverability to respond when something inevitably goes wrong.</p>
<h2>Security is a practice, not a checkbox</h2>
<p>Web application security goes much deeper than these ten concepts.</p>
<p>But these fundamentals show how security exists at different layers of an application.</p>
<ul>
<li><p>HTTPS protects information while it travels.</p>
</li>
<li><p>Authentication establishes identity, while authorization controls access.</p>
</li>
<li><p>Input validation helps protect the application from unexpected or malicious input.</p>
</li>
<li><p>Secrets management protects the credentials the application itself depends on.</p>
</li>
<li><p>Rate limiting helps prevent otherwise valid operations from being abused at scale.</p>
</li>
<li><p>Password hashing protects users' stored credentials.</p>
</li>
<li><p>Dependency management extends security considerations to third-party code</p>
</li>
<li><p>Webhook verification establishes trust with external systems.</p>
</li>
<li><p>Automated scanning adds another layer of detection.</p>
</li>
<li><p>And backups, logging, and monitoring help provide visibility and recovery when prevention isn't enough.</p>
</li>
</ul>
<p>Individually, none of these makes an application "secure." Together, they illustrate something important about application security: <strong>security isn't usually one feature or one tool. It's a collection of practices applied throughout the lifecycle of a system.</strong></p>
]]></content:encoded></item><item><title><![CDATA[Beyond Issues: 4 Linear Features for Managing Projects More Effectively]]></title><description><![CDATA[Beyond Issues: 4 Linear Features for Managing Projects More Effectively
When you first start using Linear, it's easy to think of it primarily as a place to create and track issues.
Create an issue. As]]></description><link>https://codelikeawoman.com/beyond-issues-4-linear-features-for-managing-projects-more-effectively</link><guid isPermaLink="true">https://codelikeawoman.com/beyond-issues-4-linear-features-for-managing-projects-more-effectively</guid><category><![CDATA[project management]]></category><category><![CDATA[linear]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[Developer Tools]]></category><dc:creator><![CDATA[Mirna De Jesus Cambero]]></dc:creator><pubDate>Sat, 08 Aug 2026 14:05:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67c0bd8498b78e6abd0ca1ae/21862978-3f5e-47d8-846f-85d594d438c2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Beyond Issues: 4 Linear Features for Managing Projects More Effectively</h1>
<p>When you first start using Linear, it's easy to think of it primarily as a place to create and track issues.</p>
<p>Create an issue. Assign it. Move it to <em>In Progress</em>. Mark it as <em>Done</em>.</p>
<p>That works well when the scope is small.</p>
<p>But as the work grows, a flat list of issues isn't always enough. Some work belongs to a larger project. Projects have meaningful stages. And sometimes, what looks like a single issue actually requires several steps before it can be considered done.</p>
<p>Linear has a few features that can help you organize all of this without losing sight of the actual work.</p>
<p>A quick note: I'm not affiliated with Linear. I've simply been using it across my last few jobs, including my current one. Along the way, I've been getting more familiar with both Linear and some of the project management concepts behind how work is organized.</p>
<p>Here are four features I've found particularly useful.</p>
<h2>1. Projects: Group work around a larger goal</h2>
<p>Not every issue should live independently.</p>
<p>Sometimes, a collection of issues contributes to a larger outcome: launching a mobile app, redesigning an onboarding experience, or introducing a new authentication system.</p>
<p>That's where <strong>Projects</strong> come in.</p>
<p>A project gives related work a shared home and lets you track the larger outcome instead of looking at every issue individually.</p>
<p>For example:</p>
<p><strong>Project: Launch Mobile App</strong></p>
<ul>
<li><p>Build authentication</p>
</li>
<li><p>Create onboarding experience</p>
</li>
<li><p>Implement navigation</p>
</li>
<li><p>Add user settings</p>
</li>
<li><p>Prepare app for release</p>
</li>
</ul>
<p>Instead of only asking, <em>"Which issues are done?"</em>, you can also ask:</p>
<p><em>"How is the project progressing?"</em></p>
<h2>2. Milestones: Break a project into meaningful stages</h2>
<p>A project can still contain dozens—or hundreds—of issues.</p>
<p>Milestones give you another level of organization by breaking the project into meaningful stages.</p>
<p>For our <em>Launch Mobile App</em> project, we could have:</p>
<p><strong>Milestone 1: Authentication</strong></p>
<p><strong>Milestone 2: Core Experience</strong></p>
<p><strong>Milestone 3: Beta Testing</strong></p>
<p><strong>Milestone 4: Public Launch</strong></p>
<p>Issues can then be associated with the milestone they contribute to.</p>
<p>This makes it easier to understand not only how much work remains, but <strong>where you are in the journey toward completing the project</strong>.</p>
<p>Instead of treating the entire project as one large block of work, milestones give the team smaller targets to work toward.</p>
<h2>3. Parent and sub-issues: Break complex issues into actionable work</h2>
<p>Sometimes an issue isn't really an issue.</p>
<p>It's a small project hiding inside one.</p>
<p>Imagine creating an issue called:</p>
<p><strong>Implement user authentication</strong></p>
<p>At first, that might sound like a reasonable unit of work.</p>
<p>But then you start thinking about what "done" actually requires:</p>
<ul>
<li><p>Create the login flow</p>
</li>
<li><p>Implement sign-up</p>
</li>
<li><p>Add password reset</p>
</li>
<li><p>Handle session expiration</p>
</li>
<li><p>Add authentication tests</p>
</li>
</ul>
<p>At that point, keeping everything inside one issue makes it difficult to see what's actually complete and what still needs work.</p>
<p>Instead, you can use the original issue as a <strong>parent issue</strong> and break the implementation into <strong>sub-issues</strong>.</p>
<p>The parent captures the larger piece of work.</p>
<p>The sub-issues capture the individual steps required to complete it.</p>
<p>This gives you something similar to an <strong>Epic-style structure</strong> without needing to turn every multi-step piece of work into its own project.</p>
<p>It's particularly useful when something is too large for one issue but too small to justify creating an entire project around it.</p>
<h2>4. Views: Look at the same work from different perspectives</h2>
<p>Organizing your work is only part of the problem.</p>
<p>Sometimes you simply need a better way to look at it.</p>
<p>That's where <strong>Views</strong> become useful.</p>
<p>You can filter your issues and save views based on the information you regularly care about.</p>
<p>For example:</p>
<ul>
<li><p>Issues assigned to me</p>
</li>
<li><p>Issues currently blocked</p>
</li>
<li><p>Bugs</p>
</li>
<li><p>Work ready for development</p>
</li>
<li><p>High-priority issues</p>
</li>
<li><p>Issues belonging to a particular project</p>
</li>
</ul>
<p>The underlying issues haven't changed.</p>
<p>You're simply creating different windows into the same work.</p>
<p>This becomes particularly useful as the number of issues grows. Instead of repeatedly searching or applying the same filters, you can create a view for the questions you regularly need to answer.</p>
<h2>Different tools for different levels of work</h2>
<p>The useful part isn't any individual feature. It's how they work together.</p>
<p><strong>Projects</strong> group work around a larger outcome.</p>
<p><strong>Milestones</strong> divide that outcome into meaningful stages.</p>
<p><strong>Parent and sub-issues</strong> break complex pieces of work into actionable steps.</p>
<p><strong>Views</strong> let you look at all that work from the perspective you need.</p>
<p>You don't need all of these for every project.</p>
<p>But as the work gets more complex, having different levels of organization can make it much easier to understand both the bigger picture and what needs to happen next.</p>
<p>Sometimes an issue is enough.</p>
<p>And sometimes you need to zoom out.</p>
]]></content:encoded></item><item><title><![CDATA[What Is a Backfill? Understanding a Common Data Evolution Pattern]]></title><description><![CDATA[The first time I was asked to build a backfill for a production system, I remember thinking:

This sounds important... and a little scary.

It sounded like one of those engineering terms that everyone]]></description><link>https://codelikeawoman.com/what-is-a-backfill-understanding-a-common-data-evolution-pattern</link><guid isPermaLink="true">https://codelikeawoman.com/what-is-a-backfill-understanding-a-common-data-evolution-pattern</guid><category><![CDATA[Software Engineering]]></category><category><![CDATA[Databases]]></category><category><![CDATA[data-engineering]]></category><dc:creator><![CDATA[Mirna De Jesus Cambero]]></dc:creator><pubDate>Sun, 02 Aug 2026 17:30:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67c0bd8498b78e6abd0ca1ae/9caf7b71-ab4d-4bf2-8d17-257b56e23f56.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The first time I was asked to <strong>build a backfill</strong> for a production system, I remember thinking:</p>
<blockquote>
<p><em>This sounds important... and a little scary.</em></p>
</blockquote>
<p>It sounded like one of those engineering terms that everyone else seemed to understand except me.</p>
<p>But once I learned what a backfill actually is, I realized the concept is surprisingly simple.</p>
<h2>Let's start with the word itself</h2>
<p>If English isn't your first language, the term can sound a bit mysterious.</p>
<p>Outside of software, <strong>to backfill</strong> simply means:</p>
<blockquote>
<p><strong>To fill something in after the fact.</strong></p>
</blockquote>
<p>The idea is that something already exists, but there's a gap that needs to be filled later.</p>
<p>For example:</p>
<ul>
<li><p>In construction, workers <strong>backfill</strong> a trench after installing underground pipes by filling it back in with soil.</p>
<img src="https://cdn.hashnode.com/uploads/covers/67c0bd8498b78e6abd0ca1ae/9095b6c4-580f-49ed-9c2d-20c053127ca1.png" alt="" style="display:block;margin:0 auto" />
</li>
<li><p>In human resources, companies <strong>backfill</strong> a position when they hire someone to replace an employee who left.</p>
</li>
</ul>
<p>Different contexts, same idea.</p>
<p>You're going back to fill something that wasn't filled before.</p>
<p>Software engineering borrowed exactly that idea.</p>
<h2>So... what is a backfill in software?</h2>
<p>A <strong>backfill</strong> is the process of updating historical data so it conforms to the current version of your data model.</p>
<p>Or, put another way:</p>
<blockquote>
<p><strong>It makes old data look as if today's data model had always existed.</strong></p>
</blockquote>
<p>I find this to be the simplest way to think about backfills.</p>
<h2>Why do we need them?</h2>
<p>Data models evolve.</p>
<p>No matter how carefully you design your database, requirements change.</p>
<p>You might:</p>
<ul>
<li><p>split one field into several</p>
</li>
<li><p>introduce a new table</p>
</li>
<li><p>rename a column</p>
</li>
<li><p>change how information is represented</p>
</li>
<li><p>move data to a different storage system</p>
</li>
<li><p>remove fields that are no longer needed</p>
</li>
</ul>
<p>Updating the application code is only half the job. The application now understands the new model. Your existing data doesn't.</p>
<h3>Do backfills only happen after a data model change?</h3>
<p>No. While data model evolution is the most common reason to perform a backfill, it's not the only one.</p>
<p>Teams also perform backfills when importing older historical records that weren't previously available. For example, you might initially load one year of transaction history and later decide to import the previous five years.</p>
<p>In both cases, you're filling historical gaps—just for different reasons.</p>
<h2>A practical example</h2>
<p>Imagine your application has been storing customer addresses like this for years:</p>
<pre><code class="language-text">address = "123 Main St, New York, NY"
</code></pre>
<p>Eventually, the business wants to search customers by city and state.</p>
<p>So the model evolves into:</p>
<pre><code class="language-text">street
city
state
</code></pre>
<p>Every new customer is stored using the new structure.</p>
<p>But what about the millions of customers that already exist?</p>
<p>Their data doesn't magically reorganize itself.</p>
<p>A backfill reads those historical records, extracts each component, and populates the new fields so both old and new records follow the same model.</p>
<p>Once the backfill is complete, every customer record follows the same structure, regardless of when it was created.</p>
<h2>Is a backfill the same as a data migration?</h2>
<p>Not exactly.</p>
<p>The terms are closely related and are often used interchangeably, but I find it helpful to think about them in terms of <strong>their goal</strong>.</p>
<p>A <strong>data migration</strong> typically moves or restructures data.</p>
<img src="https://cdn.hashnode.com/uploads/covers/67c0bd8498b78e6abd0ca1ae/03276c29-14aa-40d3-9444-2d01446d9887.png" alt="" />

<p>A <strong>backfill</strong> updates historical data so it matches the current version of your model.</p>
<img src="https://cdn.hashnode.com/uploads/covers/67c0bd8498b78e6abd0ca1ae/ce90d831-ac37-4963-bea2-f0287dbe71ff.png" alt="" />

<p>Many migrations include a backfill as one of their steps, but not every backfill is part of a migration.</p>
<p>Sometimes you're doing both at the same time.</p>
<h2>Why production backfills sound intimidating</h2>
<p>When someone says:</p>
<blockquote>
<p>"We need to build a backfill."</p>
</blockquote>
<p>it's easy to imagine some mysterious, high-risk engineering task.</p>
<p>In reality, the concept itself is straightforward.</p>
<p>You're simply updating historical data because your understanding of the domain has evolved.</p>
<p>Of course, executing a production backfill safely is another story.</p>
<p>Large datasets often require batching, monitoring, idempotency, retry mechanisms, and careful planning to avoid impacting users.</p>
<p>Those challenges are real.</p>
<p>But they're challenges of execution—not of the concept itself.</p>
<h2>Final thoughts</h2>
<p>Software systems evolve. Data evolves with them.
Whether that means adapting historical records to a new data model, enriching existing data, or filling in historical gaps, backfills are simply the mechanism that allows historical data to catch up.</p>
<p>Viewed through this lens, a backfill isn't an unusual operation—it's a natural part of building evolving software systems.</p>
]]></content:encoded></item><item><title><![CDATA[Your Web Side Project Doesn't Need Enterprise Infrastructure]]></title><description><![CDATA[As software engineers, the technologies we use every day inevitably shape how we think software should be built. They certainly shaped mine. At work, I'm constantly thinking about scalability, reliabi]]></description><link>https://codelikeawoman.com/your-web-side-project-doesn-t-need-enterprise-infrastructure</link><guid isPermaLink="true">https://codelikeawoman.com/your-web-side-project-doesn-t-need-enterprise-infrastructure</guid><category><![CDATA[software architecture]]></category><category><![CDATA[side project]]></category><category><![CDATA[System Design]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Cloud]]></category><category><![CDATA[Startups]]></category><dc:creator><![CDATA[Mirna De Jesus Cambero]]></dc:creator><pubDate>Tue, 21 Jul 2026 14:47:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67c0bd8498b78e6abd0ca1ae/c1136345-26f8-469f-b1a1-9392a53235b8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As software engineers, the technologies we use every day inevitably shape how we think software should be built. They certainly shaped mine. At work, I'm constantly thinking about scalability, reliability, and designing systems that can support thousands—or even millions—of users. Those are the kinds of problems enterprise software needs to solve.</p>
<p>But when I started building my own side project, I wasn't solving an enterprise problem anymore. My goal wasn't to support millions of users—it was to validate an idea, ship quickly, and keep costs low while I figured out whether anyone actually wanted to use it.</p>
<p>That shift in priorities reinforced something I'd long believed:</p>
<blockquote>
<p><strong>The right technology stack isn't universal. It depends on the stage of your project and the constraints you're working within.</strong></p>
</blockquote>
<p>Building my own side project gave me the opportunity to put that idea into practice in a very different context.</p>
<p>Here are some of the lessons I learned along the way.</p>
<h2>Choose the Framework That Helps You Ship</h2>
<p>One of the biggest decisions you'll make is choosing the technology you'll build with.</p>
<p>At work, I spend a lot of time with technologies commonly used in enterprise environments, like Java. My first instinct was to build my project using what I already knew best.</p>
<p>But then I stopped and asked myself what I was actually trying to accomplish.</p>
<p><strong>My goal wasn't to build a system capable of serving thousands of concurrent users. It was to get an idea in front of users as quickly as possible.</strong></p>
<p>That's why I chose Ruby on Rails.</p>
<p>Was I already proficient with it?</p>
<p>No.</p>
<p>But AI coding assistants made the learning curve much less intimidating. They helped me understand Rails conventions while applying concepts I already knew from years of software engineering.</p>
<p>Choosing a framework optimized for developer productivity helped me move much faster than optimizing for theoretical scalability.</p>
<hr />
<h2>Build Infrastructure for Today's Scale</h2>
<p>The next lesson was realizing that my infrastructure should reflect the scale of my project—not the scale of the systems I build professionally.</p>
<h3>Don't Pay for Idle Compute</h3>
<p>Most side projects spend the majority of their time waiting for the next user.</p>
<p>That's what makes a serverless database such a great fit.</p>
<p>Instead of paying for compute that's running 24/7, I chose Neon. Since compute only runs when the database is actually being used, costs stay incredibly low while the application is idle.</p>
<p>I also liked knowing that even if my project eventually outgrew the free tier, I wasn't going to be hit with a huge jump in costs. The pricing remains incredibly affordable, making it easy to grow without constantly worrying about the next bill.</p>
<h3>Deploy Without the Complexity</h3>
<p>Once your application is working locally, the next challenge is getting it online so other people can actually use it.</p>
<p>For my side project, I wasn't looking for a hosting platform with every possible feature or endless configuration options. I wanted something affordable that would let me deploy a container, share the application with users, and get back to building features.</p>
<p>Given those constraints, Fly.io turned out to be a great fit for my side project.</p>
<p>One of the things I liked most was how easy it was to deploy a container and immediately have a publicly accessible HTTPS application. TLS certificates are managed automatically, so I didn't have to configure or maintain them myself.</p>
<p>Another benefit was that I didn't even need to buy a domain name to get started. Fly.io provides a public HTTPS URL out of the box, making it easy to deploy and share an application while you're still validating an idea.</p>
<p>Once I was ready for a more polished experience, I purchased an inexpensive domain through Namecheap and pointed it to my application. I also set up a professional email address with Zoho Mail. Neither cost much, but together they made the project feel much more established when communicating with users.</p>
<p>In the end, hosting became something I could set up once and largely forget about, leaving me free to focus on improving the product instead of managing infrastructure.</p>
<h3>Keep Async Processing Simple</h3>
<p>As your application grows, you'll eventually need work to happen outside the request-response cycle. That might be sending emails, generating reports, processing uploads, or running scheduled tasks.</p>
<p>My application only had one background task: sending payment reminders once a day.</p>
<p>My first instinct was to spin up a dedicated worker for asynchronous processing. It worked, but I quickly realized I had created another always-on service that continuously interacted with my serverless database. That meant I wasn't just paying for the worker itself—I was also causing my database to wake up more frequently, increasing my overall costs.</p>
<p>That's when I stopped and asked myself:</p>
<blockquote>
<p><strong>Do I really need another server for this?</strong></p>
</blockquote>
<p>In my case, the answer was no.</p>
<p>Since I only needed a scheduled job once a day, GitHub Actions turned out to be a surprisingly good fit.</p>
<p>Are scheduled workflows guaranteed to run at the exact minute you specify?</p>
<p>No.</p>
<p>But if a reminder goes out at 9:18 AM instead of exactly 9:00 AM, that's perfectly acceptable for my use case.</p>
<p>GitHub Actions happened to be the solution I chose, but many hosting providers also offer scheduled jobs or cron-like functionality that can accomplish the same thing without introducing another always-on service.</p>
<p>The lesson for me was simple: not every background task needs a dedicated worker. Sometimes a scheduler is all you need.</p>
<hr />
<h2>Invest Early in Visibility</h2>
<p>Once other people start using your application, you'll eventually face a simple question:</p>
<p><strong>How will you know when something breaks?</strong></p>
<p>I didn't want to rely on users to report problems before I knew about them. Even though my project was small, I wanted the same level of visibility I value when working on enterprise systems.</p>
<p>That's why I chose Sentry.</p>
<p>What surprised me wasn't the tool itself—it was the scale.</p>
<p>In large production systems, it's not unusual to receive hundreds of error notifications in a single day. With my side project, I might only see a handful of errors in an entire month.</p>
<p>At that scale, Sentry's free tier was more than enough, and the visibility it provided was invaluable. The first time an unexpected error appeared, I knew about it immediately instead of hearing about it from a user.</p>
<p>Error monitoring was one of those enterprise practices that turned out to be surprisingly affordable at a small scale—and one I would absolutely adopt again from day one.</p>
<hr />
<h2>Spend Money Where It Solves a Real Problem</h2>
<p>Building on a budget doesn't necessarily mean spending nothing. It means being intentional about what you pay for.</p>
<p>For me, AI coding assistants fell into that category.</p>
<p>If you're only working on your project a few hours each week, even an entry-level paid plan can save hours of debugging, boilerplate code, and research.</p>
<p>I'd rather spend a small amount on a tool that helps me move faster than let the limitations of a free plan slow me down.</p>
<p>Unlike infrastructure, your time doesn't have a generous free tier.</p>
<hr />
<h2>Final Thoughts</h2>
<p>Looking back, every decision came down to asking the same question:</p>
<blockquote>
<p><strong>What problem am I actually solving today?</strong></p>
</blockquote>
<p>That simple question shaped every technical decision I made:</p>
<ul>
<li><strong>Choose the framework that helps you ship.</strong></li>
<li><strong>Build infrastructure for today's scale.</strong></li>
<li><strong>Don't pay for idle compute.</strong></li>
<li><strong>Deploy without the complexity.</strong></li>
<li><strong>Keep async processing simple.</strong></li>
<li><strong>Invest early in visibility.</strong></li>
<li><strong>Spend money where it solves a real problem.</strong></li>
</ul>
<p>None of these lessons are tied to a specific technology. They're about matching your engineering decisions to the problem you're actually trying to solve.</p>
<p>Working on enterprise systems can easily shape how we think software should be built. But side projects play by different rules. The goal is to build something useful, learn from real users, and evolve the system as it grows.</p>
<p>In addition, <strong>The right technology stack isn't universal. It depends on the stage of your project and the constraints you're working within.</strong> If your project eventually needs enterprise infrastructure, that's a wonderful problem to have.It means you've built something people actually want to use.</p>
<p>I'm curious to hear from others who have built side projects.</p>
<p><strong>What lessons have you learned about choosing technologies under different constraints? Is there a tool, service, or engineering decision that worked particularly well for you?</strong></p>
]]></content:encoded></item><item><title><![CDATA[Dead Letter Queues: The Safety Net That Only Works If Someone Checks It
]]></title><description><![CDATA[The problem it solves
Any system that processes a stream of messages—orders, events, uploads, whatever—will eventually hit one it can’t handle. Maybe the message is malformed. Maybe a downstream depen]]></description><link>https://codelikeawoman.com/dead-letter-queues-the-safety-net-that-only-works-if-someone-checks-it</link><guid isPermaLink="true">https://codelikeawoman.com/dead-letter-queues-the-safety-net-that-only-works-if-someone-checks-it</guid><category><![CDATA[distributed systems]]></category><category><![CDATA[Backend Development]]></category><category><![CDATA[messaging infrastructure]]></category><category><![CDATA[messaging]]></category><category><![CDATA[software architecture]]></category><dc:creator><![CDATA[Mirna De Jesus Cambero]]></dc:creator><pubDate>Tue, 14 Jul 2026 14:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67c0bd8498b78e6abd0ca1ae/a9379949-87d6-4f0b-94a0-4fdfb2060856.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3>The problem it solves</h3>
<p>Any system that processes a stream of messages—orders, events, uploads, whatever—will eventually hit one it can’t handle. Maybe the message is malformed. Maybe a downstream dependency is unavailable. Maybe there’s a bug nobody’s noticed yet.</p>
<p>Without a plan for this, you typically end up with one of two outcomes: the application processing the messages crashes and stops processing everything behind the bad message, or it catches the exception, logs the failure, and moves on—quietly ignoring the failed message and potentially losing data in the process.</p>
<p>The first outcome is rarely acceptable. The second is often where many of us start. Logging the exception is a good first step—it tells you something went wrong. But what if you fix the bug tomorrow? Those messages might now process just fine, yet they’re already gone.</p>
<p>That’s where a Dead Letter Queue (DLQ) comes in.</p>
<h3>The pattern</h3>
<p>A Dead Letter Queue gives failed messages a third option: park them somewhere durable, log why they failed, and keep going.</p>
<p>Why is it called a Dead Letter Queue?</p>
<p>The name comes from the postal service. A dead letter was mail that couldn’t be delivered to its intended recipient and was instead sent to a Dead Letter Office for investigation or manual handling.</p>
<p>A Dead Letter Queue follows the same idea: messages that can’t be processed are moved aside instead of being discarded, giving someone—or something—a chance to inspect and recover them later.</p>
<p>The application processing the messages isn’t blocked, no message is silently discarded, and there’s a record of exactly what went wrong and when.</p>
<p>Mechanically, it’s simple. Catch the failure, write a copy of the message (along with the error and any useful context) to storage—a dedicated queue, topic, or, in many real systems, just a database table or collection—and acknowledge the original message so the pipeline can move on to the next one.</p>
<p>A simple mental model is:</p>
<p>Things fail → retry if appropriate → if we still can’t process the message, move it to the Dead Letter Queue.</p>
<h3>The trap</h3>
<p>Here’s the part that doesn’t make it into most tutorials: writing to a DLQ is the easy half. The hard half—and the one that gets skipped surprisingly often—is having something that actually reads it back.</p>
<p>It’s an easy gap to fall into because the write side feels like “handling” the error. The code catches the exception, logs it, stores it somewhere durable—it looks resolved. But unless something is actively reading from that DLQ, replaying messages, and clearing the ones that succeed, it’s not a safety net. It’s a write-only log of things nobody’s coming back for.</p>
<p>The failures stop crashing the system, but they also stop getting fixed—they just accumulate until someone happens to go looking.</p>
<p>I once came across a production issue where one type of message kept failing during processing. The application stayed healthy because every failed message was successfully written to the DLQ. Everything appeared healthy. Yet, by the time someone checked, thousands of messages had quietly accumulated.</p>
<p>The DLQ had done exactly what it was designed to do—but nobody was looking at it.</p>
<h3>What makes a DLQ actually work</h3>
<p>A few things separate a DLQ that’s a genuine safety net from one that’s simply a collection of forgotten messages.</p>
<ol>
<li>A replay path</li>
</ol>
<p>Have a way to process failed messages again.</p>
<p>That might be a script, a scheduled job, an admin endpoint, or even a documented manual process. The important thing is that replay exists before you need it.</p>
<ol>
<li>Idempotent processing</li>
</ol>
<p>Replay only works safely if processing the same message twice doesn’t create duplicate side effects.</p>
<p>Design your processing with this in mind from the beginning—whether that’s using upserts, delete-and-recreate strategies, or another idempotent approach.</p>
<ol>
<li>Visibility</li>
</ol>
<p>Don’t just monitor whether the DLQ exists. Monitor its size and, even more importantly, whether it’s growing.</p>
<p>A DLQ with a handful of messages may not be a problem. One that’s quietly growing every day almost certainly is.</p>
<ol>
<li>Enough context to debug</li>
</ol>
<p>Saving the exception alone is a good start, but it often isn’t enough.</p>
<p>Store the information you’ll actually need later to understand the failure: identifiers, timestamps, retry count, or whatever helps explain why the message ended up there in the first place.</p>
<h3>The takeaway</h3>
<p>A DLQ is only half of the mechanism, and the write side is the easy half.</p>
<p>If you’re adding one to a system, the question worth asking isn’t “Where do failed messages go?”</p>
<p>It’s “Who, or what, brings them back?”</p>
<p>Because a Dead Letter Queue isn’t just a place to store failed messages.</p>
<p>It’s an operational workflow.</p>
<p>Without replay, visibility, and ownership, you’ve simply built a very reliable place for problems to wait until someone accidentally discovers them.</p>
]]></content:encoded></item><item><title><![CDATA[Two Ways to Control Who Can Deploy to Production in GitHub Actions]]></title><description><![CDATA[Two Ways to Control Who Can Deploy to Production in GitHub Actions
Say you have a GitHub Actions workflow that someone can manually trigger to deploy something to an environment. It could be productio]]></description><link>https://codelikeawoman.com/two-ways-to-control-who-can-deploy-to-production-in-github-actions</link><guid isPermaLink="true">https://codelikeawoman.com/two-ways-to-control-who-can-deploy-to-production-in-github-actions</guid><category><![CDATA[GitHub Actions]]></category><category><![CDATA[ci-cd]]></category><category><![CDATA[Devops]]></category><category><![CDATA[Workflow Automation]]></category><dc:creator><![CDATA[Mirna De Jesus Cambero]]></dc:creator><pubDate>Sat, 11 Jul 2026 20:03:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67c0bd8498b78e6abd0ca1ae/3f5c205f-a779-45e4-9acc-1555942d8929.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Two Ways to Control Who Can Deploy to Production in GitHub Actions</h1>
<p>Say you have a GitHub Actions workflow that someone can manually trigger to deploy something to an environment. It could be <code>production</code>, <code>staging</code>, <code>preproduction</code>, or any other environment where you want to control who can perform the deployment.</p>
<p>For this example, we'll use <code>production</code>.</p>
<p>You want to make sure that only certain people can deploy to it.</p>
<p>There are two ways you might approach this.</p>
<h2>Option 1: Check the actor in the workflow</h2>
<p>One simple solution is to keep a list of allowed users directly in the workflow and check who triggered it:</p>
<pre><code class="language-yaml">env:
  ALLOWED_ACTORS: "alice bob"

jobs:
  check-actor:
    runs-on: ubuntu-latest
    steps:
      - name: Verify actor
        env:
          ACTOR: ${{ github.actor }}
        run: |
          for a in $ALLOWED_ACTORS; do
            if [[ "$ACTOR" == "$a" ]]; then
              exit 0
            fi
          done
          echo "::error::Actor '$ACTOR' is not allowed."
          exit 1

  deploy:
    needs: check-actor
    steps: [...]
</code></pre>
<p>This is simple and it works. If the person who triggered the workflow is on the list, the deployment continues. Otherwise, it fails.</p>
<p>But there are some downsides.</p>
<p>First, the list of allowed users is hardcoded. If someone else needs permission to deploy, you have to modify the workflow file and merge that change.</p>
<p>More importantly, the protection itself lives inside the workflow. Anyone with permission to modify that file could potentially add themselves to the list, change the check, or remove it entirely.</p>
<p>For a sensitive operation like deploying to <code>production</code>, that's an important limitation.</p>
<h2>Option 2: Use GitHub environments</h2>
<p>GitHub provides a built-in way to protect deployments through <strong>environments</strong>.</p>
<p>Instead of implementing the access check yourself, your deployment job can target a configured environment:</p>
<pre><code class="language-yaml">jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps: [...]
</code></pre>
<p>The <code>production</code> environment is configured in the repository settings.</p>
<p>To configure it:</p>
<ol>
<li><p>Go to your repository on GitHub.</p>
</li>
<li><p>Open <strong>Settings → Environments</strong>.</p>
</li>
<li><p>Create or select the <code>production</code> environment.</p>
</li>
<li><p>Under <strong>Deployment protection rules</strong>, enable <strong>Required reviewers</strong>.</p>
</li>
<li><p>Add the users or teams authorized to approve deployments.</p>
</li>
<li><p>Optionally, enable <strong>Prevent self-review</strong> if you want to require someone other than the person who triggered the workflow to approve the deployment.</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/67c0bd8498b78e6abd0ca1ae/5106c0a9-3b46-4cc1-bb31-1771c1cbd4ec.png" alt="Screenshot 2026-07-11 at 3.52.10 PM" style="display:block;margin:0 auto" />

<p>Once configured, when the workflow reaches a job that targets the <code>production</code> environment, GitHub pauses the job and waits for approval from an authorized reviewer before continuing.</p>
<p>The same approach can be used for other environments. For example, you could have separate <code>staging</code>, <code>preproduction</code>, and <code>production</code> environments, each with its own protection rules and authorized reviewers.</p>
<p>This keeps the list of authorized reviewers out of the workflow and gives you built-in deployment history and traceability without having to build your own auditing mechanism.</p>
<h2>Where to see your deployment history</h2>
<p>Once you start using GitHub environments, GitHub keeps a record of your deployments.</p>
<p>You can find this history from your repository's main page, under <strong>Deployments</strong>. From there, you can see deployments to each environment and inspect details such as who triggered the deployment, which branch or commit was deployed, when it happened, whether it succeeded or failed, and who approved it when approval was required.</p>
<p>This gives you a built-in audit trail without having to create or maintain one yourself.</p>
<p>GitHub provides more details on how to <a href="https://docs.github.com/en/enterprise-cloud@latest/actions/how-tos/deploy/configure-and-manage-deployments/view-deployment-history">view deployment history</a>.</p>
<h2>Conclusion</h2>
<p>Both approaches can restrict who is allowed to deploy, but for sensitive environments like <code>production</code>, GitHub environments provide better protection and built-in traceability without requiring custom logic in your workflow.</p>
]]></content:encoded></item><item><title><![CDATA[What Is a Webhook? And Why Do They Exist?]]></title><description><![CDATA[You've probably heard the term webhook in engineering conversations and assumed it was something complicated.
It isn't.
A webhook is simply an HTTP endpoint on your server that another system calls to]]></description><link>https://codelikeawoman.com/what-is-a-webhook-and-why-do-they-exist</link><guid isPermaLink="true">https://codelikeawoman.com/what-is-a-webhook-and-why-do-they-exist</guid><category><![CDATA[webhooks]]></category><category><![CDATA[backend]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[api]]></category><category><![CDATA[http server]]></category><dc:creator><![CDATA[Mirna De Jesus Cambero]]></dc:creator><pubDate>Tue, 07 Jul 2026 14:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67c0bd8498b78e6abd0ca1ae/9cf09cf8-b965-4ee2-9970-831bc2ba3fd0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You've probably heard the term <em>webhook</em> in engineering conversations and assumed it was something complicated.</p>
<p>It isn't.</p>
<p>A webhook is simply an HTTP endpoint on your server that another system calls to notify you when something happens.</p>
<p>A simple way to think about it is as a phone number.</p>
<p>Imagine you give someone your phone number.</p>
<p>Whenever they have something to tell you, they call you.</p>
<p>A webhook works the same way.</p>
<pre><code class="language-text">Phone number      → Webhook URL
Phone call        → HTTP POST request
Caller ID         → Signature verification
"Got it."         → HTTP 200 OK
</code></pre>
<h2>When an Event Happens</h2>
<p>Suppose you're integrating with a payment provider.</p>
<p>When a payment succeeds, they send an HTTP POST request to your webhook endpoint.</p>
<p>Your application receives the request, processes it, and returns <code>200 OK</code>.</p>
<p>A simplified flow looks like this:</p>
<pre><code class="language-text">Payment succeeds
        │
        ▼
 Payment Provider
        │
 POST /webhook
        │
        ▼
 Your Application
        │
 Process event
        │
 Return 200 OK
</code></pre>
<h2>Setting Up a Webhook</h2>
<p>Before any events start flowing, there's usually a one-time setup.</p>
<p>First, you expose an HTTP endpoint in your application that will receive incoming events.</p>
<p>For example:</p>
<pre><code class="language-text">POST /webhook
</code></pre>
<p>You then provide that URL to the other system so it knows where to send events.</p>
<p>The sender also gives you a shared secret.</p>
<p>Your application uses that secret to verify that incoming requests really came from them before processing the event.</p>
<p>A typical webhook handler follows this flow:</p>
<pre><code class="language-text">Webhook arrives
        │
        ▼
Verify signature
        │
        ▼
Queue or store the work
        │
        ▼
Return 200 OK
        │
        ▼
Background worker
</code></pre>
<p>Notice that the handler itself stays lightweight.</p>
<p>Its responsibility is to acknowledge the event quickly. Any heavier processing—sending emails, updating databases, or calling other services—can happen afterward in a background worker.</p>
<h2>Why Use Webhooks?</h2>
<p>Without webhooks, your application has to keep asking another system if anything has changed.</p>
<p>This is commonly called <strong>polling</strong>.</p>
<p>With webhooks, the opposite happens.</p>
<p>The other system notifies you only when there's something new to report.</p>
<p>Here's a quick comparison:</p>
<table>
<thead>
<tr>
<th></th>
<th>Polling</th>
<th>Webhook</th>
</tr>
</thead>
<tbody><tr>
<td>Who starts the conversation?</td>
<td>Your application</td>
<td>The other system</td>
</tr>
<tr>
<td>When do you get updates?</td>
<td>On your schedule</td>
<td>When an event happens</td>
</tr>
<tr>
<td>Technology</td>
<td>HTTP</td>
<td>HTTP</td>
</tr>
</tbody></table>
<h2>Wrapping Up</h2>
<p>A webhook is simply an HTTP endpoint on your server that another system calls to notify you when something happens.</p>
<p>The technology underneath is the same HTTP you've probably been using for years.</p>
<p>The only thing that changes is who starts the conversation.</p>
]]></content:encoded></item><item><title><![CDATA[Building a Customer-Facing MCP Server: What Do You Actually Need?]]></title><description><![CDATA[In the previous article, we looked at how MCP works behind the scenes and the role played by clients, servers, tools, resources, and prompts.
This time, let's look at a specific use case: building a c]]></description><link>https://codelikeawoman.com/building-a-customer-facing-mcp-server-what-do-you-actually-need</link><guid isPermaLink="true">https://codelikeawoman.com/building-a-customer-facing-mcp-server-what-do-you-actually-need</guid><category><![CDATA[APIs]]></category><category><![CDATA[mcp]]></category><category><![CDATA[API Design]]></category><category><![CDATA[AI]]></category><category><![CDATA[ai agents]]></category><dc:creator><![CDATA[Mirna De Jesus Cambero]]></dc:creator><pubDate>Fri, 03 Jul 2026 17:20:15 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67c0bd8498b78e6abd0ca1ae/49bc3b8d-9940-488a-aeb8-7c7ccc74579a.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the previous article, we looked at how MCP works behind the scenes and the role played by clients, servers, tools, resources, and prompts.</p>
<p>This time, let's look at a specific use case: building a customer-facing MCP server.</p>
<p>If your goal is to allow customers to interact with your product through AI clients such as Claude, ChatGPT, Cursor, or Claude Code, what do you actually need?</p>
<p>The good news is that if you already have APIs or services, you're often closer than you think.</p>
<h2>The Essentials</h2>
<p>If you're building a first version, I'd focus on these first.</p>
<h3>1. Existing Functionality to Expose</h3>
<p>Most MCP servers sit on top of functionality that already exists.</p>
<p>That functionality might come from:</p>
<ul>
<li><p>REST APIs</p>
</li>
<li><p>GraphQL APIs</p>
</li>
<li><p>Databases</p>
</li>
<li><p>Internal services</p>
</li>
</ul>
<p>The MCP server becomes a layer that exposes that functionality to AI agents.</p>
<p>One thing you'll quickly discover is that not every API endpoint necessarily deserves to become a tool.</p>
<p>The goal isn't to mirror your API one-to-one. Instead, think in terms of capabilities that would be useful to an AI assistant.</p>
<p>For example, instead of exposing:</p>
<pre><code class="language-text">GET /projects
GET /projects/{id}
POST /projects/{id}/tickets
</code></pre>
<p>You might expose higher-level tools such as:</p>
<pre><code class="language-text">search_projects
create_ticket
</code></pre>
<p>The MCP server can orchestrate one or multiple API calls behind the scenes. What matters is not how your API is structured internally, but what actions are useful for the AI assistant to expose to the user.</p>
<p>One thing that surprised me was how capable modern AI clients are once they have access to well-defined tools.</p>
<p>My initial instinct was to think through every possible workflow a user might need. In practice, I found that the AI client could often orchestrate much of that on its own.</p>
<p>For example, imagine an MCP server exposes:</p>
<ul>
<li><p><code>search_projects</code></p>
</li>
<li><p><code>create_ticket</code></p>
</li>
<li><p><code>get_ticket_status</code></p>
</li>
</ul>
<p>A user might ask:</p>
<blockquote>
<p>Create a ticket for the login issue in the Mobile App project and let me know when it's done.</p>
</blockquote>
<p>The AI client can determine which tools to invoke, chain multiple calls together, provide the appropriate inputs, and even check back for updates if the workflow requires it.</p>
<p>This changed how I thought about tool design. Rather than exposing a tool for every workflow, I found it more useful to expose clear capabilities with good names and descriptions, allowing the AI client to orchestrate them as needed.</p>
<h3>2. An MCP SDK</h3>
<p>The easiest way to get started is with an MCP SDK.</p>
<p>SDKs are available for several languages, including:</p>
<ul>
<li><p>TypeScript</p>
</li>
<li><p>Python</p>
</li>
<li><p>Java</p>
</li>
</ul>
<p>The SDK handles much of the underlying protocol and transport details, allowing you to focus on defining the tools, resources, and prompts you want to expose.</p>
<h3>3. Choose a Transport</h3>
<p>One of the first architectural decisions is how clients will communicate with your MCP server.</p>
<p>The two most common options are:</p>
<ul>
<li><p><code>stdio</code></p>
</li>
<li><p><code>HTTP</code></p>
</li>
</ul>
<p>For my first implementation, I started with <code>stdio</code>.</p>
<p>It was a great way to get a prototype working quickly and test the integration in my local development environment. Since the MCP client launches the server as a local process, getting started was relatively straightforward.</p>
<p>Within a short time, I had my existing APIs connected to Claude and was able to interact with them conversationally. Instead of manually calling endpoints or testing requests, I could simply ask Claude to perform actions using the tools exposed by my MCP server.</p>
<p>It didn't take long, however, to realize that our end goal wasn't a local integration—it was allowing multiple customers to connect to the same MCP server. At that point, an HTTP-based server became the more appropriate choice.</p>
<p>If your goal is simply to get a feel for MCP and validate an integration quickly, starting with <code>stdio</code> can be a good option. It lets you connect your APIs to an AI client with minimal setup and experiment locally.</p>
<p>However, if you already know you'll need a hosted, multi-user solution, you may prefer to go straight to an HTTP-based server.</p>
<h3>4. Decide What to Expose</h3>
<p>One of the most important design decisions is deciding what functionality should be available to AI agents.</p>
<p>A common mistake is trying to expose everything.</p>
<p>For a first version, focus on the handful of actions that provide the most value.</p>
<p>Ask yourself:</p>
<blockquote>
<p>If a customer connected this MCP server today, what are the top five things they would want an AI assistant to do?</p>
</blockquote>
<p>Start there.</p>
<p>You can always expose additional functionality later.</p>
<h3>5. Decide How Users Will Authenticate</h3>
<p>If your MCP server exposes customer data or performs actions on behalf of users, you'll need a way to authenticate them.</p>
<p>Authentication becomes especially important once multiple customers start connecting to the same MCP server.</p>
<p>The AI assistant isn't acting on its own behalf—it is acting on behalf of a specific user.</p>
<p>That means the MCP server needs a way to identify who the user is and what data they are allowed to access.</p>
<p>Common approaches include:</p>
<ul>
<li><p>API Keys</p>
</li>
<li><p>OAuth</p>
</li>
</ul>
<p>We'll explore authentication in more detail in a future article.</p>
<h2>Start With the Smallest Useful Integration</h2>
<p>One thing I found useful was focusing on getting a single end-to-end workflow working first.</p>
<p>For example:</p>
<ol>
<li><p>Connect an MCP client to the server.</p>
</li>
<li><p>Authenticate a user.</p>
</li>
<li><p>Expose a small set of useful tools.</p>
</li>
<li><p>Successfully invoke those tools from the AI assistant.</p>
</li>
</ol>
<p>Once that foundation is working, it's much easier to iterate.</p>
<p>You can always add more tools, resources, prompts, richer authentication flows, and additional transports later.</p>
<p>In my experience, the challenge isn't usually exposing another tool. It's making sure the entire flow works smoothly—from connecting the client, to authenticating the user, to successfully invoking functionality in your application.</p>
<h2>Nice-to-Haves That Improve the Experience</h2>
<p>Once you have a working integration, there are several things that can make your MCP server even more effective.</p>
<h3>6. Provide Context</h3>
<p>One thing I didn't fully appreciate at first is that exposing tools is only part of the solution.</p>
<p>The AI also needs context to understand how those tools should be used.</p>
<p>For example, in one implementation, the underlying application stored dates and times in a specific timezone, while users interacted with the AI in their own local timezone.</p>
<p>Providing that context allowed the AI to perform the appropriate conversions before invoking tools or presenting results.</p>
<p>I found this particularly powerful because it allowed the AI to adapt its behavior without requiring additional tools or changes to the underlying APIs to accommodate every possible use case.</p>
<p>In many cases, if the AI seems to be making poor decisions, the missing piece isn't another capability—it's additional context.</p>
<h3>7. Consider Resources and Prompts</h3>
<p>Many MCP servers start with tools, but the protocol also supports resources and prompts.</p>
<p>Resources can provide additional context, such as product documentation, while prompts can expose reusable instructions or workflows.</p>
<p>These aren't always necessary for a first version, but they can be valuable additions as your MCP server evolves.</p>
<h3>8. Document How to Use It</h3>
<p>Even with a standardized protocol, documentation still matters.</p>
<p>Users need to know:</p>
<ul>
<li><p>What your MCP server does</p>
</li>
<li><p>How to install it</p>
</li>
<li><p>How to authenticate</p>
</li>
<li><p>Which AI tools support it</p>
</li>
</ul>
<p>Good documentation often makes the difference between an MCP server that gets adopted and one that doesn't.</p>
<h2>Conclusion</h2>
<p>One thing I learned is that a lot of MCP server design decisions become clearer through testing and real usage.</p>
<p>As you try different scenarios, you'll quickly discover whether:</p>
<ul>
<li><p>The agent is missing important context.</p>
</li>
<li><p>A tool is missing.</p>
</li>
<li><p>Two tools should be combined.</p>
</li>
<li><p>A description needs to be clearer.</p>
</li>
<li><p>Additional resources would help.</p>
</li>
</ul>
<p>I was also surprised by how capable modern AI clients are once they have access to well-defined tools. They can often chain multiple calls together, provide the appropriate inputs, and orchestrate workflows that I initially thought would require dedicated tools.</p>
<p>The best way to understand what your MCP server needs isn't to design every possible workflow upfront—it's to start with a small but useful integration and observe how the AI client uses it in practice.</p>
<p>Over time, those interactions will tell you where the gaps are and how the server should evolve.</p>
]]></content:encoded></item><item><title><![CDATA[How Does MCP Actually Work Behind the Scenes? A Simple Mental Model]]></title><description><![CDATA[In my previous article, I introduced MCP and explained why so many companies are adopting it.
But what actually happens when an AI assistant uses an MCP server?
You don't need to understand every deta]]></description><link>https://codelikeawoman.com/how-does-mcp-actually-work-behind-the-scenes-a-simple-mental-model</link><guid isPermaLink="true">https://codelikeawoman.com/how-does-mcp-actually-work-behind-the-scenes-a-simple-mental-model</guid><category><![CDATA[mcp]]></category><category><![CDATA[AI]]></category><category><![CDATA[Developer Tools]]></category><category><![CDATA[software development]]></category><category><![CDATA[ai agents]]></category><dc:creator><![CDATA[Mirna De Jesus Cambero]]></dc:creator><pubDate>Tue, 16 Jun 2026 12:25:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67c0bd8498b78e6abd0ca1ae/16d1b4b0-86e9-41b2-bde7-f7b399b31efa.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In my previous article, I introduced MCP and explained why so many companies are adopting it.</p>
<p>But what actually happens when an AI assistant uses an MCP server?</p>
<p>You don't need to understand every detail of the protocol to understand MCP. A simple mental model is enough.</p>
<h2>The Big Picture</h2>
<pre><code class="language-text">MCP Client (Claude, Cursor, ChatGPT, etc.)
                    │
                    ▼
               MCP Server
                    │
                    ▼
              Your Application
</code></pre>
<p>The MCP client is usually an AI application such as Claude, Cursor, ChatGPT, or Claude Code.</p>
<p>The MCP server sits between the AI assistant and your application. Its job is to expose information and functionality that the AI can discover and use.</p>
<p>You can think of it as another layer of abstraction between the AI client and your application's underlying APIs or services.</p>
<pre><code class="language-text">Claude
   │
   ▼
MCP Server
   │
   ▼
Your APIs / Services
</code></pre>
<p>But where does the MCP server come from?</p>
<p>Someone has to build it.</p>
<p>In many cases, the company behind the product provides an official MCP server. For example, GitHub, Linear, and other companies now offer MCP servers that expose functionality from their platforms to AI assistants.</p>
<p>If your company wants AI agents to interact with its products, your team would typically build and host an MCP server that AI clients can connect to.</p>
<p>Much like a REST API allows developers to interact with your application, an MCP server allows AI agents to interact with it.</p>
<h2>What Does an MCP Server Expose?</h2>
<p>At a high level, an MCP server can expose three types of things:</p>
<pre><code class="language-text">MCP Server
│
├── Tools
├── Resources
└── Prompts
</code></pre>
<h3>Tools</h3>
<p>Tools are actions the AI can perform.</p>
<p>Examples:</p>
<pre><code class="language-text">create_ticket
search_issues
send_message
</code></pre>
<p>When you ask Claude to create a Linear ticket, it is typically invoking a tool exposed by the Linear MCP server.</p>
<h3>Resources</h3>
<p>Resources are information the AI can read and use as context.</p>
<p>For example, imagine your company exposes its product documentation as a resource. An AI assistant could retrieve that documentation to answer questions or better understand how to use your product before invoking any tools.</p>
<p>A simple way to think about it:</p>
<ul>
<li><p>Tools → the AI wants to do something</p>
</li>
<li><p>Resources → the AI wants to know something</p>
</li>
</ul>
<h3>Prompts</h3>
<p>Prompts are reusable instructions exposed by the MCP server.</p>
<p>For example, a company might provide prompts such as:</p>
<ul>
<li><p>Summarize a project</p>
</li>
<li><p>Review code changes</p>
</li>
<li><p>Explain an error message</p>
</li>
</ul>
<p>Rather than requiring every client to define these instructions, they can be exposed directly by the MCP server.</p>
<p>A simple way to think about it:</p>
<ul>
<li><p>Tools → the AI wants to do something</p>
</li>
<li><p>Resources → the AI wants to know something</p>
</li>
<li><p>Prompts → the AI wants guidance on how to do it</p>
</li>
</ul>
<h2>How Does the AI Know What's Available?</h2>
<p>Before an AI assistant can use a tool, it first needs to discover what the MCP server provides.</p>
<p>A simplified version of that conversation looks like this:</p>
<pre><code class="language-text">Client
  │
  ├── initialize
  │
  ├── tools/list
  │
  ├── resources/list
  │
  └── prompts/list
</code></pre>
<p>Think of it as the AI asking:</p>
<blockquote>
<p>What can you do?</p>
</blockquote>
<p>The server responds with the tools, resources, and prompts it makes available.</p>
<h2>How Do Clients and Servers Communicate?</h2>
<p>All interactions between an MCP client and an MCP server happen through messages.</p>
<p>MCP standardizes these interactions using JSON-RPC, a lightweight protocol for request and response communication.</p>
<p>Those messages travel through a transport layer. MCP supports transports such as <code>stdio</code> and <code>HTTP</code>, which determine how the client and server communicate with each other.</p>
<p>Most developers interact with MCP through SDKs, which handle much of the underlying protocol and transport details for you. This allows developers to focus on the functionality they want to expose rather than the mechanics of the protocol itself.</p>
<h2>What Happens When a Tool Is Used?</h2>
<p>Let's say you ask Claude:</p>
<pre><code class="language-text">Create a Linear ticket for a login bug.
</code></pre>
<p>The flow looks something like this:</p>
<pre><code class="language-text">User
 │
 ▼
"Create a Linear ticket"
 │
 ▼
Claude
 │
 ▼
tools/call(create_ticket)
 │
 ▼
Linear MCP Server
 │
 ▼
Linear
</code></pre>
<p>Claude invokes the appropriate tool, the MCP server performs the operation, and the result is returned back to Claude.</p>
<h2>Bringing It All Together</h2>
<pre><code class="language-text">           MCP Client
      (Claude, Cursor, etc.)
                   │
                   ▼
             MCP Server
        ┌──────┼──────┐
        ▼      ▼      ▼
      Tools Resources Prompts
                   │
                   ▼
      APIs / Databases / Services
</code></pre>
<p>The client discovers what the server exposes and uses tools, resources, or prompts as needed.</p>
<p>That's the core mental model behind MCP.</p>
<p>In the next article, we'll look at what you actually need to build your own MCP server.</p>
]]></content:encoded></item><item><title><![CDATA[What Is MCP? The Protocol Behind Many AI Coding Integrations]]></title><description><![CDATA[About four months ago, I came across the term MCP for the first time while working on a project that required building an MCP server so customers could integrate AI agents with their time tracking dat]]></description><link>https://codelikeawoman.com/what-is-mcp-the-protocol-behind-many-ai-coding-integrations</link><guid isPermaLink="true">https://codelikeawoman.com/what-is-mcp-the-protocol-behind-many-ai-coding-integrations</guid><category><![CDATA[mcp]]></category><category><![CDATA[AI]]></category><category><![CDATA[Developer Tools]]></category><dc:creator><![CDATA[Mirna De Jesus Cambero]]></dc:creator><pubDate>Sat, 13 Jun 2026 15:34:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67c0bd8498b78e6abd0ca1ae/95809933-ea78-4248-b072-41c1847b0154.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>About four months ago, I came across the term <strong>MCP</strong> for the first time while working on a project that required building an MCP server so customers could integrate AI agents with their time tracking data and automate workflows.</p>
<p>After shipping the first version, I started noticing MCP everywhere. GitHub has one. Linear has one. Slack, Notion, databases, browser automation tools—the list keeps growing. It seems like every week another company is making their functionalities available through MCP.</p>
<p>So what exactly is MCP?</p>
<h2>What Is MCP?</h2>
<p>MCP (Model Context Protocol) is a lightweight standard that allows AI agents to discover and use the actions an application makes available, such as creating a ticket, searching for issues, retrieving customer data, or updating a record.</p>
<p>Many companies already expose this functionality —often referred to as capabilities— through REST or GraphQL APIs. The challenge is not that the functionality doesn't exist—it's that every API exposes it differently. MCP provides a standard way for AI agents to discover what's available, understand how to invoke it, and interact with it consistently.</p>
<blockquote>
<p>REST APIs are typically designed for developers. MCP servers are designed for AI agents.</p>
</blockquote>
<h2>How Does It Work?</h2>
<p>A typical setup looks something like this:</p>
<pre><code class="language-text">AI Assistant
      ↓
  MCP Server
      ↓
 REST / GraphQL API
      ↓
 Application
</code></pre>
<p>The MCP server exposes information and functionality that the AI assistant can discover and use.</p>
<p>When a user asks the assistant to perform an action or retrieve information, the assistant can interact with the underlying application through the MCP server.</p>
<h2>MCP Usage in Practice</h2>
<p>Many popular products now provide official MCP integrations, including:</p>
<ul>
<li><p>GitHub</p>
</li>
<li><p>Linear</p>
</li>
<li><p>Slack</p>
</li>
<li><p>Notion</p>
</li>
<li><p>Stripe</p>
</li>
<li><p>Browser automation tools</p>
</li>
</ul>
<p>Let's look at a concrete example using the Linear MCP with Claude Code.</p>
<h3>Step 1: Install the Linear MCP</h3>
<pre><code class="language-bash">claude mcp add linear
</code></pre>
<p>Or simply ask Claude:</p>
<pre><code class="language-text">Install the Linear MCP for me.
</code></pre>
<p>Claude will guide you through the installation and authentication process.</p>
<h3>Step 2: Use It</h3>
<p>Once connected, you can ask things like:</p>
<p><strong>View your open issues</strong></p>
<pre><code class="language-text">What are my open Linear issues?
</code></pre>
<p><strong>Create a ticket</strong></p>
<pre><code class="language-text">Create a high-priority bug ticket for users being logged out unexpectedly.
</code></pre>
<p><strong>Search sprint work</strong></p>
<pre><code class="language-text">Show me all issues assigned to me this sprint.
</code></pre>
<p>From the user's perspective, it feels like a conversation.</p>
<p>For example, when you ask Claude to create a Linear ticket, Claude sends the request to the Linear MCP server. The MCP server exposes actions such as creating tickets or searching issues and knows how to translate those requests into operations in Linear.</p>
<p>##If AI agents can already use APIs, why do we need MCP?</p>
<p>AI agents can interact with REST and GraphQL APIs directly, so MCP isn't solving a technical limitation.</p>
<p>The value of MCP is that it gives AI agents a predictable way to discover what an application can do and how to use it.</p>
<p>For example, an AI assistant could be connected to GitHub, Linear, Slack, and dozens of other tools. Without a common standard, each integration would expose its functionality differently. MCP provides a consistent way for AI agents to discover available actions and interact with those systems.</p>
<h2>Why Developers Should Care</h2>
<p>MCP is quickly becoming one of the standard ways companies expose their products to AI agents. If your company provides APIs, there's a good chance you'll encounter MCP as organizations look for ways to make their products accessible to AI-powered tools and workflows.</p>
<p>Even if you're not building MCP servers yourself, you can benefit from the growing ecosystem of MCP integrations. By connecting AI assistants to tools you already use—such as GitHub, Linear, Slack, or Notion—you can automate repetitive tasks, retrieve information more easily, and streamline everyday workflows.</p>
<p>Whether you're building products or simply using them, MCP is becoming an increasingly important part of how AI systems interact with software.</p>
]]></content:encoded></item><item><title><![CDATA[Git Worktrees: A Git Feature I Wish I Had Tried Earlier]]></title><description><![CDATA[I had heard about Git worktrees before, but I didn’t really try them until recently — when a coding assistant I was using created one naturally as part of its workflow.
I was working on a feature when]]></description><link>https://codelikeawoman.com/git-worktrees-a-git-feature-i-wish-i-had-tried-earlier</link><guid isPermaLink="true">https://codelikeawoman.com/git-worktrees-a-git-feature-i-wish-i-had-tried-earlier</guid><category><![CDATA[Git]]></category><category><![CDATA[software development]]></category><category><![CDATA[Developer Tools]]></category><category><![CDATA[version control]]></category><category><![CDATA[developer productivity]]></category><category><![CDATA[AI-assisted development]]></category><category><![CDATA[AI]]></category><category><![CDATA[claude]]></category><dc:creator><![CDATA[Mirna De Jesus Cambero]]></dc:creator><pubDate>Tue, 26 May 2026 19:14:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67c0bd8498b78e6abd0ca1ae/c8906a52-4b91-4064-9d8f-bc314ad280c8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I had heard about Git worktrees before, but I didn’t really try them until recently — when a coding assistant I was using created one naturally as part of its workflow.</p>
<p>I was working on a feature when we realized there was a bug fix that needed to happen first because it was a prerequisite.</p>
<p>And that’s when worktrees clicked for me.</p>
<h2>So… what is a Git worktree?</h2>
<p>A Git worktree lets you have <strong>multiple working directories of the same repository on your computer</strong>, each checked out to a different branch.</p>
<p>Instead of constantly switching branches in one folder, you can have something like this:</p>
<pre><code class="language-plaintext">/my-app-main
/my-app-feature
/my-app-bugfix
</code></pre>
<p>All connected to the same Git repository — but each on a different branch.</p>
<h2>Without worktrees</h2>
<p>Imagine you’re halfway through building a feature:</p>
<pre><code class="language-plaintext">git checkout feature/new-reporting
</code></pre>
<p>Then you realize a bug fix must happen first because it’s blocking the feature.</p>
<p>Normally, the flow might look something like this:</p>
<pre><code class="language-plaintext">git stash
git checkout main
git pull
git checkout -b fix/auth-token
</code></pre>
<p>Fix the bug, push it, merge it… then come back:</p>
<pre><code class="language-plaintext">git checkout feature/new-reporting
git stash pop
</code></pre>
<p>Totally workable.</p>
<p>But there’s friction.</p>
<p>You interrupt your flow, mentally bookmark where you were, and juggle branch switching, stashes, and working state.</p>
<h2>With worktrees</h2>
<p>Instead, you can keep your feature exactly where it is and create a separate working directory for the bug fix.</p>
<p>From your repo:</p>
<pre><code class="language-plaintext">git worktree add ../my-app-bugfix -b fix/auth-token main
</code></pre>
<p>Now you have:</p>
<pre><code class="language-plaintext">/my-app-feature   → feature/new-reporting
/my-app-bugfix    → fix/auth-token
</code></pre>
<p>You can literally open both folders side by side.</p>
<p>Your feature stays untouched while you work on the prerequisite bug fix. When the fix is merged, you can update your feature branch and continue.</p>
<p>You’re still context switching.</p>
<p>But it feels much more graceful.</p>
<p>Especially with coding assistants, I’ve found this surprisingly helpful.</p>
<p>It makes parallel threads of work feel much more natural.</p>
<p>For example, I might be actively building a feature in one worktree while a separate Claude session works on a prerequisite bug fix in another — all within the same repository, but isolated from each other.</p>
<p>Or I might spin up a temporary worktree for an experiment without disrupting the work already in progress.</p>
<p>You’re still coordinating and context switching, but the friction feels much lower.</p>
<h2>A small practice that seems helpful</h2>
<p>It could be helfpul s keep a <strong>default worktree always synced with</strong> <code>main</code>, something like:</p>
<pre><code class="language-plaintext">/my-app-main      → always synced with main
/my-app-feature   → current feature
/my-app-hotfix    → urgent fix
</code></pre>
<p>That way, creating new branches or experiments feels lightweight and you always have a clean starting point.</p>
<p>It’s one of those Git features I wish I had tried earlier.</p>
<p>Sharing in case it’s useful for someone else too.</p>
]]></content:encoded></item><item><title><![CDATA[How I’ve Been Working With Claude Code (and What’s Helped So Far)]]></title><description><![CDATA[What Changed in My Workflow After Using Claude Code
Claude Code has quickly become one of the go-to coding assistants for many developers and teams. I’ve been using it as part of my day-to-day workflo]]></description><link>https://codelikeawoman.com/how-i-ve-been-working-with-claude-code-and-what-s-helped-so-far</link><guid isPermaLink="true">https://codelikeawoman.com/how-i-ve-been-working-with-claude-code-and-what-s-helped-so-far</guid><category><![CDATA[AI]]></category><category><![CDATA[claude-code]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[Developer Tools]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[claude]]></category><dc:creator><![CDATA[Mirna De Jesus Cambero]]></dc:creator><pubDate>Wed, 29 Apr 2026 00:36:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67c0bd8498b78e6abd0ca1ae/0a3c1899-facf-4cc3-b8ca-14be6df99377.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>What Changed in My Workflow After Using Claude Code</h1>
<p>Claude Code has quickly become one of the go-to coding assistants for many developers and teams. I’ve been using it as part of my day-to-day workflow, and over time I’ve noticed that it hasn’t just changed how I code, but also how I approach building software.</p>
<p>This is not meant to be a set of best practices or anything definitive—just a collection of things that have genuinely helped me so far, in case they’re useful to someone else as well.</p>
<p>I’ve found my way of working with it gradually shifting:</p>
<ul>
<li><p>from using it for isolated tasks</p>
</li>
<li><p>to being more intentional about planning and context</p>
</li>
<li><p>to using it for refinement and quality improvements</p>
</li>
<li><p>and finally toward thinking more automation-first in general</p>
</li>
</ul>
<h2>1. Getting comfortable with sessions and keeping context</h2>
<p>One of the first things that made a difference for me was just getting comfortable with how sessions work, and how to recover or continue work without losing context.</p>
<h3><code>/login</code> helps you recover without losing context</h3>
<p>If you ever need to switch accounts or your session expires, you can use:</p>
<p><code>/login</code></p>
<p>This lets you reauthenticate without losing your current context.</p>
<p>Useful when:</p>
<ul>
<li><p>switching subscriptions or accounts</p>
</li>
<li><p>dealing with expired sessions</p>
</li>
<li><p>recovering access without restarting your work</p>
</li>
</ul>
<p>Small thing, but I’ve found it helps maintain flow.</p>
<h3><code>claude --resume</code> is a safety net (with one catch)</h3>
<p>Another thing that has been really helpful is being able to resume sessions.</p>
<p>You can use:</p>
<p><code>claude --resume</code></p>
<p>to return to a previous session.</p>
<p>This has been especially useful when things go wrong:</p>
<ul>
<li><p>accidental terminal closes</p>
</li>
<li><p>machine restarts</p>
</li>
<li><p>terminal freezes (this has happened to me more than a handful of times)</p>
</li>
</ul>
<p>It’s also useful when working on different things and wanting to keep contexts separated across sessions.</p>
<p>One thing I didn’t realize at first:</p>
<blockquote>
<p><code>--resume</code> is scoped per directory.</p>
</blockquote>
<p>If you’re not in the same directory where you started the session, you won’t see it. I learned this empirically after thinking I had lost everything—when I was just in the wrong folder.</p>
<h3><code>/btw</code> helps you keep momentum</h3>
<p>More recently, I’ve also found <code>/btw</code> useful.</p>
<p>It lets you ask questions or leave comments without interrupting the current work.</p>
<p>Instead of breaking the flow of a task, you can surface clarifications while the assistant continues working.</p>
<p>It’s a small thing, but it helps keep iteration smooth.</p>
<p>There are many other commands and I haven’t explored most of them yet, but I’m curious to keep discovering more over time.</p>
<h2>2. Learning to guide the assistant better</h2>
<h3>Plan before implementing</h3>
<p>I’ve found it works much better to align on a plan before jumping into implementation.</p>
<p>Before coding, I usually ask Claude to outline:</p>
<ul>
<li><p>the approach</p>
</li>
<li><p>the scope</p>
</li>
<li><p>the main steps</p>
</li>
</ul>
<p>This tends to lead to more predictable outcomes and fewer surprises later.</p>
<p>Otherwise, you risk getting code that technically works but doesn’t match how you actually wanted things to be structured or behave.</p>
<h3>Provide the right context, not more context</h3>
<p>Coding assistants have gotten really good—but the outcome is still very tied to the input.</p>
<p>When it makes sense, I try to include things like:</p>
<ul>
<li><p>ticket context (title + description)</p>
</li>
<li><p>PRDs or specs</p>
</li>
<li><p>related PRs or discussions</p>
</li>
<li><p>incident reports</p>
</li>
<li><p>logs or traces</p>
</li>
<li><p>specific files or references</p>
</li>
</ul>
<p>The goal is to help build the right context around the problem.</p>
<p>That said, this is very dependent on what you’re building. For simpler tasks, you don’t need much input to get a correct implementation.</p>
<p>But as things get more complex, the difference becomes obvious.</p>
<blockquote>
<p>It’s not about adding more input—it’s about adding the right input.</p>
</blockquote>
<p>I like to think of it as getting an incident report with no repro steps or logs—you’d be lost. And the same applies to the agent.</p>
<h3>Challenge limitations instead of accepting them</h3>
<p>Sometimes the agent will say it can’t do something and suggest doing it manually in another terminal.</p>
<p>In those cases, I don’t always take that at face value.</p>
<p>There have been situations where:</p>
<ul>
<li><p>it actually can do it within the current context</p>
</li>
<li><p>it has done similar things before</p>
</li>
<li><p>or the limitation isn’t entirely clear</p>
</li>
</ul>
<p>So instead, I try to push a bit:</p>
<ul>
<li><p>“Can you try doing this directly?”</p>
</li>
<li><p>“What exactly is preventing you from doing it here?”</p>
</li>
</ul>
<p>Not in a forceful way, but just to better understand what’s going on and avoid unnecessary manual work when possible.</p>
<h3>Use Claude for refinement, not just implementation</h3>
<p>Another part of how I use Claude is not just to build things, but to refine and improve them.</p>
<p>This can include:</p>
<ul>
<li><p>system architecture</p>
</li>
<li><p>code structure and conventions</p>
</li>
<li><p>testing strategy</p>
</li>
<li><p>performance considerations</p>
</li>
<li><p>maintainability and scalability</p>
</li>
<li><p>security and documentation</p>
</li>
</ul>
<p>These are all things I used to think through more manually, and now I find it helpful to involve the agent in that process as well.</p>
<p>It doesn’t replace thinking, but it helps get to a more polished result faster.</p>
<h3>Preferences help, but don’t always persist</h3>
<p>Another thing I do is ask the agent to keep certain preferences in mind.</p>
<p>This might include:</p>
<ul>
<li><p>coding style</p>
</li>
<li><p>architectural choices</p>
</li>
<li><p>naming conventions</p>
</li>
<li><p>testing expectations</p>
</li>
</ul>
<p>It can help reduce repetition and keep things more aligned with how I like to work.</p>
<p>That said, it’s not perfect. It doesn’t always remember or apply these consistently across sessions, so I do find myself repeating things.</p>
<p>I do think this is an area where things can improve, and I’m curious to explore better ways of handling this over time.</p>
<h2>3. Shifting toward automation-first thinking</h2>
<p>One of the bigger shifts for me has been moving away from doing things manually by default.</p>
<p>Now I tend to delegate a lot of repetitive or operational work to the assistant:</p>
<ul>
<li><p>creating feature branches</p>
</li>
<li><p>opening pull requests</p>
</li>
<li><p>drafting documents</p>
</li>
<li><p>running repetitive git tasks</p>
</li>
<li><p>setting up GitHub workflows</p>
</li>
</ul>
<p>And more generally, it’s changed how I think about work.</p>
<p>Now when I see something manual, my first thought is:</p>
<blockquote>
<p>can this be automated?</p>
</blockquote>
<p>Sometimes that leads to:</p>
<ul>
<li><p>writing a small shell script</p>
</li>
<li><p>creating aliases</p>
</li>
<li><p>setting environment variables</p>
</li>
<li><p>or adding a GitHub workflow to handle it</p>
</li>
</ul>
<p>Individually these are small improvements, but over time they reduce a lot of friction.</p>
<h2>4. Using different tools depending on the stage of work</h2>
<p>Another thing I do sometimes is use non-coding agents, like ChatGPT, for earlier stages of thinking before switching to Claude for implementation.</p>
<p>This has been especially useful for:</p>
<ul>
<li><p>brainstorming ideas</p>
</li>
<li><p>light planning</p>
</li>
<li><p>product and UX thinking</p>
</li>
<li><p>system architecture design</p>
</li>
</ul>
<p>Not all work starts with code. Some of it benefits from a more open-ended space before getting into implementation details.</p>
<p>Once things feel clearer, I then move into Claude to actually build, refine, and execute.</p>
<p>It’s a simple separation, but I’ve found it helps me keep better structure between:</p>
<ul>
<li><p>thinking and shaping the problem</p>
</li>
<li><p>and actually building the solution</p>
</li>
</ul>
<h2>Final thoughts</h2>
<p>These are just some of the things that have helped me so far while working with Claude.</p>
<p>What stands out most to me is that this is still evolving. The more I use it, the more I notice small adjustments in how I think about building, structuring, and automating work.</p>
<p>Some of these things I’ve learned through trial and error, and others have come more naturally as I’ve kept incorporating AI into my day-to-day workflow.</p>
<p>I’m sure this will continue to evolve over time—and I’d be curious to hear how others are approaching it as well.</p>
]]></content:encoded></item><item><title><![CDATA[Building My First Side Project: From Intuition to a Repeatable Way of Thinking]]></title><description><![CDATA[This is the first time I’m intentionally working on a side project end-to-end — outside of a company, outside of a team, and outside of an existing product system.
It’s just me, an idea, and the respo]]></description><link>https://codelikeawoman.com/building-my-first-side-project-from-intuition-to-a-repeatable-way-of-thinking</link><guid isPermaLink="true">https://codelikeawoman.com/building-my-first-side-project-from-intuition-to-a-repeatable-way-of-thinking</guid><category><![CDATA[AI Coding Assistant]]></category><category><![CDATA[product thinking]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[side project]]></category><category><![CDATA[Startups]]></category><category><![CDATA[AI in Development]]></category><category><![CDATA[prd document ]]></category><dc:creator><![CDATA[Mirna De Jesus Cambero]]></dc:creator><pubDate>Fri, 24 Apr 2026 17:24:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/67c0bd8498b78e6abd0ca1ae/98aa0875-ad0b-4200-82a6-a2f22cc534e6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is the first time I’m intentionally working on a side project end-to-end — outside of a company, outside of a team, and outside of an existing product system.</p>
<p>It’s just me, an idea, and the responsibility of turning it into something real.</p>
<p>At first, I assumed the challenge would be execution: writing code and building features quickly.</p>
<p>But I’ve realized something more subtle:</p>
<blockquote>
<p>The hardest part is not building the feature — it’s deciding what to build, and how small that version needs to be to validate real user behavior.</p>
</blockquote>
<p>This is just one of several challenges I’m starting to recognize, and I suspect there are more worth writing about as I keep building.</p>
<hr />
<h2>How I’ve Always Thought About Features</h2>
<p>I’ve always approached features with a consistent mental model.</p>
<p>Whenever I work on something, I naturally think about:</p>
<ul>
<li><p>why it exists and what value it creates</p>
</li>
<li><p>what problem it solves</p>
</li>
<li><p>how it can be simplified</p>
</li>
<li><p>what edge cases matter</p>
</li>
<li><p>what I explicitly don’t want to build</p>
</li>
<li><p>how it fits into the existing data model and UI</p>
</li>
<li><p>how the user experience should feel end-to-end</p>
</li>
<li><p>how to implement it with minimal user friction</p>
</li>
<li><p>what signals would indicate this feature is actually working</p>
</li>
</ul>
<p>This isn’t something I learned recently — it’s just how I reason about problems.</p>
<p>Even in my engineering role, I’ve always used a strong user-story mindset: understanding intent, questioning value, and sometimes even challenging whether something should be built at all.</p>
<p>Over time, I realized this is what more senior thinking actually looks like:</p>
<blockquote>
<p>Not just implementing features, but deeply understanding intent and improving it when needed.</p>
</blockquote>
<hr />
<h2>The Problem I Didn’t Have a Name For</h2>
<p>In my side project, I started running into a subtle issue.</p>
<p>I would do most of the feature thinking upfront — naturally and consistently — before writing any code: planning how it would fit into the existing data model, how it would behave in the UI, and what the user experience would feel like end-to-end.</p>
<p>But once I started implementing, that thinking had nowhere to live.</p>
<p>If I stored it in a Linear ticket, it would get buried in execution details. More importantly, a Linear ticket — as a small unit of work — often didn’t represent the full end-to-end feature vision, only the initial increment being implemented.</p>
<p>If I stored it in Google Docs, it would quickly become something I forgot to revisit.</p>
<p>So I was left with a gap:</p>
<blockquote>
<p>A lot of clarity before coding… and very little preservation of that clarity afterward.</p>
</blockquote>
<hr />
<h2>Seeing PRDs Differently</h2>
<p>I’ve seen PRDs before — usually as input documents in product discussions or strategic meetings. But I never had to write them myself.</p>
<p>For context, a <strong>PRD (Product Requirements Document)</strong> is a structured document used to define what a product or feature should do, why it exists, and how success is measured before development begins.</p>
<p>They felt formal, structured, and somewhat distant from how I naturally think when building.</p>
<p>That changed in this side project.</p>
<p>Because I am now both the product thinker and the implementer, I started noticing that I may need a simple framework to structure this type of thinking, and a persistent place to store it so it could eventually be referenced when implementing features in Linear.</p>
<p>Not as something I fully have — but as something that might be useful to formalize.</p>
<hr />
<h2>PRD-Lite: A Repeatable Thinking System (Emerging Idea)</h2>
<p>Over time, I noticed I was already doing a fairly repeatable type of thinking — just informally.</p>
<p>To make it easier to reason about, I started sketching a simple structure I might use for future features. I’m considering storing it as a Markdown file in the repo so it stays close to the code and could eventually be referenced during implementation.</p>
<p>I haven’t adopted this yet, but it feels like a direction worth exploring.</p>
<h3>🧾 PRD-lite format</h3>
<ol>
<li><p><strong>Why it exists</strong> Why this should be built and what value it creates</p>
</li>
<li><p><strong>MVP scope</strong> What the smallest useful version includes</p>
</li>
<li><p><strong>User flow</strong> What the user does</p>
</li>
<li><p><strong>Data model (light)</strong> What objects exist</p>
</li>
<li><p><strong>Out of scope</strong> What I am explicitly not building</p>
</li>
</ol>
<p>Right now, this is more of a thinking scaffold than a system — something I use to avoid losing structure while I’m reasoning about a feature.</p>
<hr />
<h2>The AI Layer: A Possible Extension of This Idea</h2>
<p>Another thing I’ve been thinking about is how this kind of structured thinking might interact with AI coding assistants.</p>
<p>If I eventually write these PRD-like notes in the repo, they could serve as useful context when working with tools like Claude or similar assistants.</p>
<p>Instead of re-explaining intent in prompts, the assistant could potentially use:</p>
<ul>
<li><p>structured feature definitions</p>
</li>
<li><p>scope boundaries</p>
</li>
<li><p>user flows</p>
</li>
<li><p>data model assumptions</p>
</li>
</ul>
<p>This is not something I’m actively doing yet — but it’s one of the reasons this idea feels interesting.</p>
<p>It suggests a workflow where thinking becomes more portable between planning and implementation.</p>
<hr />
<h2>Why This Matters in a Side Project</h2>
<p>When you’re building alone, there is no separation between:</p>
<ul>
<li><p>product thinking</p>
</li>
<li><p>decision making</p>
</li>
<li><p>implementation</p>
</li>
</ul>
<p>Everything collapses into one continuous flow.</p>
<p>That makes it very easy to:</p>
<ul>
<li><p>lose intent while coding</p>
</li>
<li><p>overbuild in the moment</p>
</li>
<li><p>forget original assumptions</p>
</li>
<li><p>drift away from the real problem you were solving</p>
</li>
</ul>
<p>What I’m exploring here is not a process — it’s a way to preserve intent long enough to actually build from it.</p>
<hr />
<h2>Final Thought</h2>
<p>I used to think PRDs were documents created for engineers to follow.</p>
<p>Now I see them differently.</p>
<p>They are not external instructions — they are a way to make your thinking persistent enough to actually build from it.</p>
<p>And for the first time building something on my own, I’ve realized I’ve naturally been doing product thinking all along — and I’m now starting to explore how to structure that thinking so it doesn’t get lost between idea and implementation.</p>
<p>Think clearly. Write it down. Build intentionally.</p>
<p>That’s the system I’m starting to discover</p>
]]></content:encoded></item><item><title><![CDATA[The Unspoken Feelings of a Senior Developer Working With AI-Powered Tools]]></title><description><![CDATA[AI-powered tools have changed how we work — significantly.
They help us explore faster, reduce friction, catch bugs in seconds, diagnose issues in minutes, refactor code precisely, and move through iterations at a pace that would have felt unrealisti...]]></description><link>https://codelikeawoman.com/the-unspoken-feelings-of-a-senior-developer-working-with-ai-powered-tools</link><guid isPermaLink="true">https://codelikeawoman.com/the-unspoken-feelings-of-a-senior-developer-working-with-ai-powered-tools</guid><category><![CDATA[senior-developers]]></category><category><![CDATA[AI]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[developer experience]]></category><dc:creator><![CDATA[Mirna De Jesus Cambero]]></dc:creator><pubDate>Fri, 16 Jan 2026 21:25:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768598600847/6ef50785-2dce-4a79-bc48-eb3cef6c3ba3.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>AI-powered tools have changed how we work — significantly.</p>
<p>They help us explore faster, reduce friction, catch bugs in seconds, diagnose issues in minutes, refactor code precisely, and move through iterations at a pace that would have felt unrealistic not long ago.</p>
<p>All of that is objectively positive.</p>
<p>And yet, alongside those gains, something quieter has shifted — not in capability, but in <strong>how the work feels</strong>.</p>
<p>This article is an attempt to name that shift from the perspective of a senior developer working with AI-powered tools — not as a critique, but as a reflection on the feelings and navigation required in this new paradigm.</p>
<hr />
<h2 id="heading-from-building-systems-to-shaping-them">From building systems to shaping them</h2>
<p>For much of my career, the relationship between effort and ownership was relatively clear.</p>
<p>You analyzed and built solutions.<br />You carried problems end to end.<br />You found and fixed issues opportunely.<br />You mastered your stack, sharpened your understanding of computer science, system design, and engineering — and used that as a guiding signal of how good you were becoming.</p>
<p>Today, that relationship is less predictable.</p>
<p>AI doesn’t just help write code — it can:</p>
<ul>
<li><p>propose the structure</p>
</li>
<li><p>draft the solution</p>
</li>
<li><p>fill in edge cases</p>
</li>
<li><p>produce something that already works</p>
</li>
</ul>
<p>When that happens, the output can be correct and high quality — yet still feel oddly distant.</p>
<p>We’ve shifted from knowing things at a very granular level to <strong>orchestrating tools and shaping systems into existence</strong>.</p>
<hr />
<h2 id="heading-when-contribution-starts-to-feel-ambiguous">When contribution starts to feel ambiguous</h2>
<p>I’ve noticed that how I feel about my contribution depends less on how much AI I use, and more on <strong>how I’m involved</strong>.</p>
<p>I feel more grounded when I:</p>
<ul>
<li><p>intentionally orchestrate the process</p>
</li>
<li><p>participate actively in decision-making</p>
</li>
<li><p>debug and investigate issues</p>
</li>
<li><p>guide refactors toward something more mature</p>
</li>
<li><p>go beyond the first iteration</p>
</li>
<li><p>push for quality, correctness, tests, observability, and other core software engineering practices</p>
</li>
</ul>
<p>In those moments, the work feels <strong>shaped</strong>, not just completed.</p>
<p>By contrast, when AI:</p>
<ul>
<li><p>proposes the structure</p>
</li>
<li><p>drafts most of the solution</p>
</li>
<li><p>resolves edge cases</p>
</li>
<li><p>delivers something that already works</p>
</li>
<li><p>resolves things quickly</p>
</li>
</ul>
<p>…and my role is mainly to approve, lightly edit, and merge, a different feeling can emerge:</p>
<blockquote>
<p><em>“I didn’t really contribute enough.”</em></p>
</blockquote>
<p>The difference isn’t about typing versus not typing.<br />It’s about <strong>being necessary in the process</strong>.</p>
<hr />
<h2 id="heading-how-ai-can-aggravate-impostor-syndrome">How AI can aggravate impostor syndrome</h2>
<p>This is where impostor syndrome can quietly enter — even for experienced developers.</p>
<p>Not because the work is poor.<br />Not because the system is fragile.<br />But because familiar signals of contribution are no longer reliable.</p>
<p>When AI takes a large portion of the work off my plate, it can provoke thoughts like:</p>
<ul>
<li><p>“I didn’t do this myself.”</p>
</li>
<li><p>“I couldn’t have done this without help.”</p>
</li>
<li><p>“How much of this was really me?”</p>
</li>
</ul>
<p>The system may be better.<br />The outcome may be right.<br />But the internal sense of contribution can lag behind.</p>
<p>At times, this can feel like a lack of competence — but it’s really a <strong>paradigm shift</strong>.</p>
<p>Ways of recognizing value that worked for more than a decade — attention to detail, deeply caring about correctness and quality — don’t always map cleanly onto AI-assisted workflows or speedy shipment.</p>
<p>That mismatch can create doubt or unease, even when everything is working as intended.</p>
<hr />
<h2 id="heading-a-different-shape-of-ownership">A different shape of ownership</h2>
<p>At senior levels, ownership no longer means:</p>
<ul>
<li><p>writing every line</p>
</li>
<li><p>remembering every detail</p>
</li>
<li><p>coming up with the architecture entirely on your own</p>
</li>
</ul>
<p>It means:</p>
<ul>
<li><p>defining the right problem</p>
</li>
<li><p>choosing the right constraints</p>
</li>
<li><p>rejecting wrong or premature solutions</p>
</li>
<li><p>integrating pieces into a coherent system</p>
</li>
<li><p>being accountable when it breaks</p>
</li>
</ul>
<p>When something fails, no one asks how much AI was involved.<br />They ask <strong>you</strong> — the engineer who shipped the solution — to stand behind it.</p>
<p>That responsibility hasn’t gone away, even if the path to it looks different now.</p>
<hr />
<h2 id="heading-closing">Closing</h2>
<p>I don’t think I’m alone in experiencing this shift.</p>
<p>I suspect many senior developers are navigating something similar: working effectively with powerful tools, delivering solid systems, and still adjusting to how contribution and ownership <em>feel</em> in this new context.</p>
<p>This article itself was written with AI assistance — intentionally. Not to dilute the voice, but to reflect the reality it describes. The thoughts captured here represent feelings I’ve been sitting with as I navigate this new paradigm.</p>
<p>If this resonates, I’d be genuinely interested in hearing how others are experiencing this shift — what feels familiar, what feels different, and how you’re making sense of authorship and ownership in AI-assisted work.</p>
]]></content:encoded></item><item><title><![CDATA[Modern Java Feature: String Templates (Preview)]]></title><description><![CDATA[Update – December 2024:String Templates were available as a preview feature in Java 21 and Java 22.However, the feature was withdrawn from Java 23 because the JDK team decided it required more design work before moving forward.The concept is not canc...]]></description><link>https://codelikeawoman.com/modern-java-feature-string-templates-preview</link><guid isPermaLink="true">https://codelikeawoman.com/modern-java-feature-string-templates-preview</guid><category><![CDATA[Java]]></category><category><![CDATA[Modern Java]]></category><category><![CDATA[string templates]]></category><category><![CDATA[clean code]]></category><dc:creator><![CDATA[Mirna De Jesus Cambero]]></dc:creator><pubDate>Mon, 24 Nov 2025 13:03:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1764021053920/ddc7f1c5-fa5d-459e-b817-d8c22c71d181.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p><strong>Update – December 2024:</strong><br />String Templates were available as a preview feature in Java 21 and Java 22.<br />However, the feature was <strong>withdrawn from Java 23</strong> because the JDK team decided it required more design work before moving forward.<br />The concept is not canceled — it is expected to return in a future release with an improved design — but it is <strong>not available in Java 23</strong>.</p>
<p>Everything described in this article refers to the earlier preview versions (Java 21–22). I’m keeping the article online for educational purposes and will update it again when the new design becomes available.</p>
</blockquote>
<p>String Templates are one of the most anticipated additions to modern Java. They were first introduced as a preview feature in <strong>Java 21</strong>, refined again in <strong>Java 22</strong>, and kept in preview in <strong>Java 23</strong>. They are expected to remain in preview until at least Java 24 or 25, when the design stabilizes enough to be finalized.</p>
<p>Even as a preview feature, String Templates are worth trying out. They significantly improve how developers construct strings in Java, and as a result, you will be better prepared when they are finalized.</p>
<h2 id="heading-what-is-this-feature-about">What is this feature about?</h2>
<p>String Templates allow embedding Java expressions directly inside string literals using the <code>STR</code> processor. This provides cleaner syntax for building structured strings and complements Text Blocks, making Java feel more expressive and modern.</p>
<p><strong>Quick example:</strong></p>
<pre><code class="lang-java">String name = <span class="hljs-string">"Mirna"</span>;
String message = STR.<span class="hljs-string">"Hello, \{name}! Today is \{LocalDate.now()}"</span>;
System.out.println(message);
</code></pre>
<p><strong>Sample output:</strong></p>
<pre><code class="lang-java">Hello, Mirna! Today is <span class="hljs-number">2025</span>-<span class="hljs-number">01</span>-<span class="hljs-number">01</span>
</code></pre>
<p><strong>How to run preview features:</strong><br />If you are trying this with Java 21 or newer, you must enable preview mode, for a class for code that uses this feature like StringTemplatesDemo, we will need:</p>
<pre><code class="lang-java">javac --enable-preview --release <span class="hljs-number">21</span> StringTemplatesDemo.java
java  --enable-preview StringTemplatesDemo
</code></pre>
<p>Adjust the version number if you use a newer JDK (22, 23, etc.).</p>
<hr />
<h1 id="heading-common-use-cases"><strong>Common Use Cases</strong></h1>
<h2 id="heading-1-build-json-cleanly">1. Build JSON cleanly</h2>
<pre><code class="lang-java">String user = <span class="hljs-string">"Mirna"</span>;
<span class="hljs-keyword">int</span> score = <span class="hljs-number">98</span>;

String json = STR.<span class="hljs-string">""</span><span class="hljs-string">"
{
  "</span>user<span class="hljs-string">": "</span>\{user}<span class="hljs-string">",
  "</span>score<span class="hljs-string">": \{score},
  "</span>active<span class="hljs-string">": true
}
"</span><span class="hljs-string">""</span>;

System.out.println(json);
</code></pre>
<h2 id="heading-2-construct-sql-dynamically">2. Construct SQL dynamically</h2>
<pre><code class="lang-java">String table = <span class="hljs-string">"users"</span>;
String email = <span class="hljs-string">"mirna@example.com"</span>;

String query = STR.<span class="hljs-string">""</span><span class="hljs-string">"
SELECT *
FROM \{table}
WHERE email = '\{email}'
"</span><span class="hljs-string">""</span>;

System.out.println(query);
</code></pre>
<h2 id="heading-3-build-html-fragments">3. Build HTML fragments</h2>
<pre><code class="lang-java">String name = <span class="hljs-string">"Mirna"</span>;

String html = STR.<span class="hljs-string">""</span><span class="hljs-string">"
&lt;p&gt;Hello, &lt;b&gt;\{name}&lt;/b&gt;!&lt;/p&gt;
"</span><span class="hljs-string">""</span>;

System.out.println(html);
</code></pre>
<h2 id="heading-4-use-expressions-directly">4. Use expressions directly</h2>
<pre><code class="lang-java">String math = STR.<span class="hljs-string">"2 + 2 = \{2 + 2}"</span>;
String random = STR.<span class="hljs-string">"Random value: \{Math.round(Math.random() * 10)}"</span>;

System.out.println(math);
System.out.println(random);
</code></pre>
<h2 id="heading-5-use-methods-inside-templates">5. Use methods inside templates</h2>
<pre><code class="lang-java"><span class="hljs-function">String <span class="hljs-title">greet</span><span class="hljs-params">(String name)</span> </span>{
    <span class="hljs-keyword">return</span> STR.<span class="hljs-string">"Hello, \{name.toUpperCase()}!"</span>;
}

System.out.println(greet(<span class="hljs-string">"Mirna"</span>));
</code></pre>
<h2 id="heading-6-combine-text-blocks-templates">6. Combine Text Blocks + Templates</h2>
<pre><code class="lang-java">String language = <span class="hljs-string">"Java"</span>;
String feature = <span class="hljs-string">"String Templates"</span>;

String article = STR.<span class="hljs-string">""</span><span class="hljs-string">"
## Feature Highlight

We're exploring \{feature} in \{language}.

Advantages:
- Readable
- Multiline
- Interpolated directly in code
"</span><span class="hljs-string">""</span>;

System.out.println(article);
</code></pre>
<h1 id="heading-final-thoughts">Final Thoughts</h1>
<p>String Templates make Java more modern and readable, especially when combined with Text Blocks. The feature is a meaningful improvement over string concatenation and <code>String.format</code>.</p>
<p>There are still two things that feel a bit unnatural to me.<br />First, the backslash before the curly braces (<code>\{name}</code>). Second, having to prefix a Text Block with <code>STR."""</code> just to enable interpolation. Most modern languages do not require this extra step. I can live with both, though, since the improvement in readability is real.</p>
<p>I’m excited to see how this feature evolves as it moves toward finalization.</p>
]]></content:encoded></item><item><title><![CDATA[Using Cursor as my AI pair-programmer — a personal take]]></title><description><![CDATA[I’ve been using Cursor as part of my daily development flow and wanted to share what’s worked (and what hasn’t) from my own experience. I’ll keep this casual and short.
Quick note: I’m not affiliated with Cursor in any way. This is just my honest exp...]]></description><link>https://codelikeawoman.com/using-cursor-as-my-ai-pair-programmer-a-personal-take</link><guid isPermaLink="true">https://codelikeawoman.com/using-cursor-as-my-ai-pair-programmer-a-personal-take</guid><category><![CDATA[cursor IDE]]></category><category><![CDATA[#ai-tools]]></category><category><![CDATA[ai agents]]></category><category><![CDATA[pair programming]]></category><category><![CDATA[Junior developer ]]></category><dc:creator><![CDATA[Mirna De Jesus Cambero]]></dc:creator><pubDate>Sun, 28 Sep 2025 14:25:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1759069375045/83ee80e1-93a4-4b6f-8b53-af92bf96b3df.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I’ve been using Cursor as part of my daily development flow and wanted to share what’s worked (and what hasn’t) from my own experience. I’ll keep this casual and short.</p>
<p>Quick note: I’m not affiliated with Cursor in any way. This is just my honest experience using it as part of my daily development flow.</p>
<h2 id="heading-quick-intro-what-is-cursor">Quick intro — what is Cursor?</h2>
<p>Cursor is an AI-powered coding editor that acts like an extra (very eager) developer: it reads your repo, runs agent-style tasks, edits files, suggests fixes, and can even scaffold features. The real game changer for me is that Cursor <em>has the whole codebase as context</em>. You don’t need to paste files or explain the entire project — you just reference what you want, and it knows what you mean.</p>
<h2 id="heading-pricing">Pricing</h2>
<p>Cursor has a free option and paid tiers. Honestly, the free tier didn’t help me much beyond getting a feel for the tool. The real value came once I switched to the paid plan, and I’m grateful my team enabled it for us. On paid plans you get a limited number of requests each month, and you can also set an extra buffer of requests if someone goes over — which is especially useful for teams.</p>
<hr />
<h2 id="heading-my-overall-impression">My overall impression</h2>
<p>I’m genuinely impressed. When you give Cursor a clear task with a simple scope and good instructions, it can really deliver. Some people compare it to having a junior dev working for you — eager to move fast and do as much as possible. I completely agree.</p>
<h2 id="heading-where-cursor-really-helped-me">Where Cursor really helped me</h2>
<ul>
<li><p><strong>Bug fixing:</strong> Quick suggestions to try out, which makes debugging less painful.</p>
</li>
<li><p><strong>Asking questions about the codebase:</strong> Instead of searching forever, I just ask Cursor about specific parts of the repo.</p>
</li>
<li><p><strong>Implementing new features with clear scope:</strong> It’s great at well-defined, non-ambiguous tasks.</p>
</li>
<li><p><strong>Remembering syntax/commands:</strong> Especially across multiple languages and tools.</p>
</li>
<li><p><strong>Running commands directly:</strong> This feature has been surprisingly useful. Instead of switching back and forth, I can run commands right from Cursor and keep my flow.</p>
</li>
</ul>
<h2 id="heading-but-its-not-perfect">But it’s not perfect</h2>
<ul>
<li><p>It makes mistakes and sometimes gives “bandaid” fixes (like just adding a null check without solving the real root cause).</p>
</li>
<li><p>It can get stuck repeating the same approach or stuck in general</p>
</li>
<li><p>You still need to test, review, and ask hard questions. Think of it as a junior dev — you’re still the one driving.</p>
</li>
<li><p>It might overengineer solutions at times</p>
</li>
<li><p>AI hallucinates and gives you wrong answers or partially right ones</p>
</li>
</ul>
<hr />
<h2 id="heading-things-that-helped-me-get-more-out-of-cursor">Things that helped me get more out of Cursor</h2>
<ul>
<li><p><strong>Split complex tasks into steps.</strong> Easier to validate each result.</p>
</li>
<li><p><strong>Use git intentionally.</strong> Feature branches, small commits, incremental progress, stashes, etc to record the steps that are in a good shape or acceptably good to go. That will save you when you need to reverse things safely</p>
</li>
<li><p><strong>Run QA and tests.</strong> Don’t trust the shiny “all fixed” message until you confirm.</p>
</li>
<li><p><strong>Ask for tests.</strong> Make Cursor write them, then review and improve.</p>
</li>
<li><p><strong>Provide detailed context.</strong> Logs, corner cases, knowns/unknowns, filenames, code fragment — the more thoughtful you are at providing guidance, the better the output.</p>
</li>
<li><p><strong>Track usage.</strong> Requests are limited, so I keep an eye on how many I’ve used.</p>
</li>
<li><p><strong>Pair it with ChatGPT.</strong> I use ChatGPT for general questions or planning, Cursor for codebase-aware tasks.</p>
</li>
<li><p><strong>Be specific when reverting changes.</strong> Otherwise, it might undo more than you expect.</p>
</li>
<li><p><strong>Batch steps into one request when possible.</strong> Saves credits.</p>
</li>
<li><p><strong>Keep learning prompt engineering.</strong> Makes a big difference in quality.</p>
</li>
<li><p><strong>Know when to pivot.</strong> If it’s stuck, switch strategies or take over manually.</p>
</li>
<li><p><strong>Still use docs/Stack Overflow/debugging.</strong> AI is great, but not everything.</p>
</li>
</ul>
<p>And yes — don’t waste requests saying “thank you” 😅.</p>
<hr />
<h2 id="heading-final-thoughts">Final thoughts</h2>
<p>I’d say Cursor is <strong>absolutely worth it</strong>. You just need to learn how to use it well: give it scope, guide it, question it, and keep testing. The better you are at driving, the more you’ll get out of it. For me, it’s taken a lot of the heavy lifting and repetitive tasks off my plate, while still leaving me in control of the important decisions.  </p>
<p>What about you? Have you tried Cursor as your AI pair-programmer? How do you use it day-to-day? I would love to hear your experience as well!</p>
]]></content:encoded></item><item><title><![CDATA[Modern Java Feature: Text Blocks]]></title><description><![CDATA[Java developers have long struggled with multi-line strings containing escaped characters and awkward formatting. Enter Text Blocks, a modern Java feature that makes multi-line string handling cleaner, more readable, and more expressive.
🆕 What Is a...]]></description><link>https://codelikeawoman.com/modern-java-feature-text-blocks</link><guid isPermaLink="true">https://codelikeawoman.com/modern-java-feature-text-blocks</guid><category><![CDATA[text blocks]]></category><category><![CDATA[java strings]]></category><category><![CDATA[Modern Java]]></category><dc:creator><![CDATA[Mirna De Jesus Cambero]]></dc:creator><pubDate>Tue, 22 Jul 2025 13:12:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1753190645882/db639e67-391c-4ab4-ae51-af45cb6894df.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Java developers have long struggled with multi-line strings containing escaped characters and awkward formatting. Enter <strong>Text Blocks</strong>, a modern Java feature that makes multi-line string handling <em>cleaner, more readable, and more expressive</em>.</p>
<h2 id="heading-what-is-a-text-block">🆕 What Is a Text Block?</h2>
<p>A <strong>text block</strong> is a multi-line string literal introduced as a <strong>preview in Java 13</strong> and <strong>officially added in Java 15</strong>. It allows you to write strings across multiple lines without having to escape every newline or quote.</p>
<h3 id="heading-syntax">Syntax</h3>
<p>Text blocks are enclosed in <strong>three double quotes (</strong><code>"""</code>), and must start on a <strong>new line</strong>:</p>
<pre><code class="lang-java">String html = <span class="hljs-string">""</span><span class="hljs-string">"
    &lt;html&gt;
        &lt;body&gt;
            &lt;h1&gt;Hello, World!&lt;/h1&gt;
        &lt;/body&gt;
    &lt;/html&gt;
    "</span><span class="hljs-string">""</span>;
</code></pre>
<p>This is far more readable than:</p>
<pre><code class="lang-java">String html = <span class="hljs-string">"&lt;html&gt;\n"</span> +
              <span class="hljs-string">"    &lt;body&gt;\n"</span> +
              <span class="hljs-string">"        &lt;h1&gt;Hello, World!&lt;/h1&gt;\n"</span> +
              <span class="hljs-string">"    &lt;/body&gt;\n"</span> +
              <span class="hljs-string">"&lt;/html&gt;\n"</span>;
</code></pre>
<hr />
<h3 id="heading-where-can-you-use-a-text-block">Where Can You Use a Text Block?</h3>
<p>Anywhere you can use a regular string, you can use a text block. That includes:</p>
<ul>
<li><p>Assignments to variables</p>
</li>
<li><p>Method arguments</p>
</li>
<li><p>Return values</p>
</li>
<li><p>Annotations</p>
</li>
<li><p>Logs and templates</p>
</li>
</ul>
<pre><code class="lang-java">System.out.println(<span class="hljs-string">""</span><span class="hljs-string">"
    This is a valid
    text block string
    used as an argument.
    "</span><span class="hljs-string">""</span>);
</code></pre>
<p>Text blocks shine when dealing with structured or long-form content:</p>
<ul>
<li><p>🖥️ <strong>HTML/XML/JSON snippets</strong></p>
</li>
<li><p>💾 <strong>SQL queries</strong></p>
</li>
<li><p>🤖 <strong>Prompt engineering for AI</strong></p>
</li>
<li><p>🧑‍🏫 <strong>Code samples for documentation</strong></p>
</li>
<li><p>📜 <strong>Poetry, song lyrics, or long messages</strong></p>
</li>
</ul>
<pre><code class="lang-java">String sql = <span class="hljs-string">""</span><span class="hljs-string">"
    SELECT id, name
    FROM users
    WHERE active = 1
    ORDER BY name;
    "</span><span class="hljs-string">""</span>;
</code></pre>
<hr />
<h2 id="heading-smart-whitespace-handling-incidental-vs-essential">Smart Whitespace Handling: Incidental vs Essential</h2>
<p>One of the most powerful features of text blocks is their ability to <strong>remove incidental white space</strong>—the indentation that's not part of the string itself but exists to align with your code.</p>
<p>Let’s visualize this with <code>·</code> characters to represent leading spaces.</p>
<h3 id="heading-example-incidental-white-space-removed">👇 Example: Incidental White Space Removed</h3>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">writeHTML</span><span class="hljs-params">()</span> </span>{
    String html = <span class="hljs-string">""</span><span class="hljs-string">"
········&lt;html&gt;
········    &lt;body&gt;
········        &lt;p&gt;Hello World.&lt;/p&gt;
········    &lt;/body&gt;
········&lt;/html&gt;
········"</span><span class="hljs-string">""</span>;
    writeOutput(html);
}
</code></pre>
<p>✅ The output of <code>html</code> becomes:</p>
<pre><code class="lang-plaintext">&lt;html&gt;
    &lt;body&gt;
        &lt;p&gt;Hello World.&lt;/p&gt;
    &lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>Java determines the <em>minimum indentation</em> level (based on the closing delimiter position) and removes it from all lines. This keeps your string readable <em>and</em> aligned with your code.</p>
<h3 id="heading-preserving-essential-white-space">Preserving Essential White Space</h3>
<p>To preserve indentation within the string, just move your content to the right:</p>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">writeHTML</span><span class="hljs-params">()</span> </span>{
    String html = <span class="hljs-string">""</span><span class="hljs-string">"
········    &lt;html&gt;
········        &lt;body&gt;
········            &lt;p&gt;Hello World.&lt;/p&gt;
········        &lt;/body&gt;
········    &lt;/html&gt;
········"</span><span class="hljs-string">""</span>;
    writeOutput(html);
}
</code></pre>
<p>Result:</p>
<pre><code class="lang-plaintext">    &lt;html&gt;
        &lt;body&gt;
            &lt;p&gt;Hello World.&lt;/p&gt;
        &lt;/body&gt;
    &lt;/html&gt;
</code></pre>
<h3 id="heading-opting-out-of-trimming-entirely">Opting Out of Trimming Entirely</h3>
<p>If you don’t want Java to trim anything, place the closing delimiter on the far left:</p>
<pre><code class="lang-java"><span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">writeHTML</span><span class="hljs-params">()</span> </span>{
    String html = <span class="hljs-string">""</span><span class="hljs-string">"
                  &lt;html&gt;
                      &lt;body&gt;
                          &lt;p&gt;Hello World.&lt;/p&gt;
                      &lt;/body&gt;
                  &lt;/html&gt;
"</span><span class="hljs-string">""</span>;
    writeOutput(html);
}
</code></pre>
<p>Java will treat every character as-is, preserving all leading spaces.</p>
<blockquote>
<p>For all the nitty-gritty details, see JEP 355.</p>
</blockquote>
<hr />
<h3 id="heading-normalize-line-terminators">Normalize Line Terminators</h3>
<p>Text blocks use <code>\n</code> (line feed) as the default line terminator on all platforms. If you need to convert them to your system-specific line separator (e.g. <code>\r\n</code> on Windows), you can do:</p>
<pre><code class="lang-java">String adjusted = textBlock.replaceAll(<span class="hljs-string">"\n"</span>, System.lineSeparator());
</code></pre>
<p>This ensures your strings behave consistently across environments.</p>
<hr />
<h2 id="heading-new-string-methods-introduced">New String Methods Introduced</h2>
<p>Java 15 introduced several useful methods in the <code>String</code> class to complement and enhance the text block feature:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Method</td><td>Description</td></tr>
</thead>
<tbody>
<tr>
<td><code>stripIndent()</code></td><td>Removes incidental indentation from a multi-line string</td></tr>
<tr>
<td><code>translateEscapes()</code></td><td>Converts escape sequences like <code>\\n</code> or <code>\\t</code> into actual characters</td></tr>
<tr>
<td><code>formatted(Object...)</code></td><td>A more readable alternative to <code>String.format()</code>, used directly on string instances</td></tr>
</tbody>
</table>
</div><h4 id="heading-example-formatted-with-text-blocks">Example: 📄 <code>formatted(...)</code> with Text Blocks</h4>
<p>You can use <code>.formatted(...)</code> directly on a text block to inject dynamic values:</p>
<pre><code class="lang-java">String email = <span class="hljs-string">""</span><span class="hljs-string">"
    Dear %s,

    Your order #%d has been shipped and is expected to arrive by %s.

    Thank you for shopping with us!
    "</span><span class="hljs-string">""</span>.formatted(<span class="hljs-string">"Mirna"</span>, <span class="hljs-number">12345</span>, <span class="hljs-string">"Friday"</span>);

System.out.println(email);
</code></pre>
<p><strong>Output:</strong></p>
<pre><code class="lang-plaintext">Dear Mirna,

Your order #12345 has been shipped and is expected to arrive by Friday.

Thank you for shopping with us!
</code></pre>
<p>This makes text blocks a great fit for templated emails, prompts, or code generation — with clean syntax and no clutter.</p>
<h2 id="heading-official-documentation">📚 Official Documentation</h2>
<p>Want to learn more or see additional examples? Check out the official guide:<br />👉 <a target="_blank" href="https://docs.oracle.com/en/java/javase/15/text-blocks/index.html">Text Blocks in Java 15</a></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Text blocks are a small yet powerful improvement in Java that makes your code easier to read, write, and maintain—especially when working with structured or formatted content.</p>
<p>No more escaping newlines or cramming everything into one line. Just write clean, readable text!</p>
<p>🧭 Stay tuned for more in the <em>Modern Java Features</em> series!</p>
]]></content:encoded></item></channel></rss>